fix: simplify workbench session list (#1285)
This commit is contained in:
@@ -116,7 +116,7 @@ function traceRecord(trace: RunnerTrace | null | undefined): Record<string, unkn
|
||||
</template>
|
||||
</li>
|
||||
</ol>
|
||||
<p v-if="readableRows.length === 0" class="trace-empty">等待后端事件。</p>
|
||||
<p v-if="readableRows.length === 0" class="trace-empty">思考中...</p>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { useWorkbenchStore } from "@/stores/workbench";
|
||||
import { useClipboard } from "@/composables/useClipboard";
|
||||
import { formatRelativeTime } from "@/utils";
|
||||
|
||||
const workbench = useWorkbenchStore();
|
||||
const { copied, copy } = useClipboard();
|
||||
@@ -51,6 +50,18 @@ function readWidth(): number {
|
||||
return 300;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSessionUpdatedTime(value: string | null | undefined): string {
|
||||
if (!value) return "未记录";
|
||||
const date = new Date(value);
|
||||
if (!Number.isFinite(date.getTime())) return value;
|
||||
const delta = Date.now() - date.getTime();
|
||||
if (delta < 60_000) return "刚刚";
|
||||
if (delta < 3_600_000) return `${Math.max(1, Math.round(delta / 60_000))} 分`;
|
||||
if (delta < 86_400_000) return `${Math.max(1, Math.round(delta / 3_600_000))} 时`;
|
||||
if (delta < 604_800_000) return `${Math.max(1, Math.round(delta / 86_400_000))} 天`;
|
||||
return date.toLocaleString(undefined, { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,11 +75,9 @@ function readWidth(): number {
|
||||
</div>
|
||||
<div class="session-model-channel" id="session-model-channel">模型通道:{{ workbench.providerProfile }}</div>
|
||||
<div class="session-list" id="session-tabs">
|
||||
<button v-for="tab in workbench.sessionTabs" :key="tab.conversationId" class="session-tab" :data-active="tab.active" type="button" @click="workbench.selectConversation(tab)">
|
||||
<strong>{{ tab.label || tab.conversationId }}</strong>
|
||||
<span>{{ tab.sessionId || "未绑定 session" }}</span>
|
||||
<span>{{ tab.subtitle }}</span>
|
||||
<small>{{ formatRelativeTime(tab.updatedAt || tab.startedAt) }}</small>
|
||||
<button v-for="tab in workbench.sessionTabs" :key="tab.conversationId" class="session-tab" :data-active="tab.active" type="button" :title="tab.label || tab.conversationId" @click="workbench.selectConversation(tab)">
|
||||
<span class="session-tab-title">{{ tab.label || tab.conversationId }}</span>
|
||||
<time class="session-tab-time" :datetime="tab.updatedAt || tab.startedAt || undefined">{{ formatSessionUpdatedTime(tab.updatedAt || tab.startedAt) }}</time>
|
||||
</button>
|
||||
<p v-if="workbench.sessionTabs.length === 0" class="muted-box">没有 session。发送前必须显式新建或选择 session。</p>
|
||||
</div>
|
||||
|
||||
@@ -93,20 +93,20 @@ export function conversationToSessionTab(conversation: ConversationRecord, activ
|
||||
const status = firstNonEmptyString(conversation.snapshot?.sessionStatus, conversation.status, conversation.messages?.at(-1)?.status) ?? "source";
|
||||
const trace = firstNonEmptyString(conversation.lastTraceId, conversation.messages?.at(-1)?.traceId);
|
||||
const userMessage = conversation.messages?.find((message) => message.role === "user");
|
||||
const preview = firstReadableText(
|
||||
const preview = firstReadableSentence(
|
||||
conversation.firstUserMessagePreview,
|
||||
conversation.snapshot?.firstUserMessagePreview,
|
||||
conversation.userPreview,
|
||||
conversation.title,
|
||||
conversation.name,
|
||||
userMessage?.text,
|
||||
userMessage?.content,
|
||||
userMessage?.title
|
||||
userMessage?.title,
|
||||
conversation.title,
|
||||
conversation.name
|
||||
);
|
||||
return {
|
||||
...conversation,
|
||||
key: sessionId ?? conversationId,
|
||||
label: truncateLabel(preview) ?? `Session ${shortToken(sessionId ?? conversationId, 8)}`,
|
||||
label: preview ?? `Session ${shortToken(sessionId ?? conversationId, 8)}`,
|
||||
subtitle: `${status === "running" ? "处理中" : trace ? `trace ${shortToken(trace, 8)}` : "未开始"} · ${conversation.messageCount ?? conversation.messages?.length ?? 0} 条`,
|
||||
sessionId,
|
||||
threadId: firstNonEmptyString(conversation.threadId, conversation.session?.threadId),
|
||||
@@ -215,31 +215,21 @@ function compareProviderProfileOptions(left: ProviderProfileOption, right: Provi
|
||||
return left.value.localeCompare(right.value);
|
||||
}
|
||||
|
||||
function truncateLabel(value: string | null): string | null {
|
||||
if (!value) return null;
|
||||
const text = value.replace(/\s+/gu, " ").trim();
|
||||
if (!text || text === OBJECT_OBJECT_TEXT) return null;
|
||||
return text.length <= 36 ? text : `${text.slice(0, 36).trimEnd()}...`;
|
||||
}
|
||||
|
||||
function firstReadableText(...values: unknown[]): string | null {
|
||||
function firstReadableSentence(...values: unknown[]): string | null {
|
||||
for (const value of values) {
|
||||
const text = readableText(value);
|
||||
const text = readableSentence(value);
|
||||
if (text) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readableText(value: unknown): string | null {
|
||||
if (typeof value === "string") {
|
||||
const text = value.replace(/\s+/gu, " ").trim();
|
||||
return text && text !== OBJECT_OBJECT_TEXT ? text : null;
|
||||
}
|
||||
function readableSentence(value: unknown): string | null {
|
||||
if (typeof value === "string") return firstSentenceFromText(value);
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (Array.isArray(value)) return firstReadableText(...value);
|
||||
if (Array.isArray(value)) return firstReadableSentence(...value);
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const record = value as Record<string, unknown>;
|
||||
return firstReadableText(
|
||||
return firstReadableSentence(
|
||||
record.text,
|
||||
record.content,
|
||||
record.message,
|
||||
@@ -252,6 +242,24 @@ function readableText(value: unknown): string | null {
|
||||
);
|
||||
}
|
||||
|
||||
function firstSentenceFromText(value: string): string | null {
|
||||
const normalized = value.replace(/\u0000/gu, " ").replace(/\r\n|\r/gu, "\n").replace(/[\t\f\v ]+/gu, " ").trim();
|
||||
if (!normalized || normalized === OBJECT_OBJECT_TEXT) return null;
|
||||
const candidates = [
|
||||
sentenceBoundary(normalized, /[。!?!?]/u, 1),
|
||||
sentenceBoundary(normalized, /\.(?=\s|$)/u, 1),
|
||||
sentenceBoundary(normalized, /\n+/u, 0)
|
||||
].filter((index): index is number => typeof index === "number");
|
||||
const end = candidates.length > 0 ? Math.min(...candidates) : normalized.length;
|
||||
const text = normalized.slice(0, end).replace(/\s+/gu, " ").trim();
|
||||
return text && text !== OBJECT_OBJECT_TEXT ? text : null;
|
||||
}
|
||||
|
||||
function sentenceBoundary(value: string, pattern: RegExp, offset: number): number | null {
|
||||
const match = pattern.exec(value);
|
||||
return match?.index === undefined ? null : match.index + offset;
|
||||
}
|
||||
|
||||
function shortToken(value: string | null | undefined, size: number): string {
|
||||
const text = String(value ?? "");
|
||||
if (text.length <= size) return text;
|
||||
|
||||
@@ -606,13 +606,16 @@
|
||||
}
|
||||
|
||||
.session-tab {
|
||||
display: grid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
width: 100%;
|
||||
gap: 4px;
|
||||
gap: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
padding: 10px;
|
||||
padding: 7px 9px;
|
||||
color: #334155;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -622,16 +625,27 @@
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.session-tab strong,
|
||||
.session-tab span,
|
||||
.session-tab small {
|
||||
.session-tab-title {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-tab span,
|
||||
.session-tab small,
|
||||
.session-tab-time {
|
||||
flex: 0 0 auto;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.muted-box {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user