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>
|
</template>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p v-if="readableRows.length === 0" class="trace-empty">等待后端事件。</p>
|
<p v-if="readableRows.length === 0" class="trace-empty">思考中...</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useWorkbenchStore } from "@/stores/workbench";
|
import { useWorkbenchStore } from "@/stores/workbench";
|
||||||
import { useClipboard } from "@/composables/useClipboard";
|
import { useClipboard } from "@/composables/useClipboard";
|
||||||
import { formatRelativeTime } from "@/utils";
|
|
||||||
|
|
||||||
const workbench = useWorkbenchStore();
|
const workbench = useWorkbenchStore();
|
||||||
const { copied, copy } = useClipboard();
|
const { copied, copy } = useClipboard();
|
||||||
@@ -51,6 +50,18 @@ function readWidth(): number {
|
|||||||
return 300;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -64,11 +75,9 @@ function readWidth(): number {
|
|||||||
</div>
|
</div>
|
||||||
<div class="session-model-channel" id="session-model-channel">模型通道:{{ workbench.providerProfile }}</div>
|
<div class="session-model-channel" id="session-model-channel">模型通道:{{ workbench.providerProfile }}</div>
|
||||||
<div class="session-list" id="session-tabs">
|
<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)">
|
<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)">
|
||||||
<strong>{{ tab.label || tab.conversationId }}</strong>
|
<span class="session-tab-title">{{ tab.label || tab.conversationId }}</span>
|
||||||
<span>{{ tab.sessionId || "未绑定 session" }}</span>
|
<time class="session-tab-time" :datetime="tab.updatedAt || tab.startedAt || undefined">{{ formatSessionUpdatedTime(tab.updatedAt || tab.startedAt) }}</time>
|
||||||
<span>{{ tab.subtitle }}</span>
|
|
||||||
<small>{{ formatRelativeTime(tab.updatedAt || tab.startedAt) }}</small>
|
|
||||||
</button>
|
</button>
|
||||||
<p v-if="workbench.sessionTabs.length === 0" class="muted-box">没有 session。发送前必须显式新建或选择 session。</p>
|
<p v-if="workbench.sessionTabs.length === 0" class="muted-box">没有 session。发送前必须显式新建或选择 session。</p>
|
||||||
</div>
|
</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 status = firstNonEmptyString(conversation.snapshot?.sessionStatus, conversation.status, conversation.messages?.at(-1)?.status) ?? "source";
|
||||||
const trace = firstNonEmptyString(conversation.lastTraceId, conversation.messages?.at(-1)?.traceId);
|
const trace = firstNonEmptyString(conversation.lastTraceId, conversation.messages?.at(-1)?.traceId);
|
||||||
const userMessage = conversation.messages?.find((message) => message.role === "user");
|
const userMessage = conversation.messages?.find((message) => message.role === "user");
|
||||||
const preview = firstReadableText(
|
const preview = firstReadableSentence(
|
||||||
conversation.firstUserMessagePreview,
|
conversation.firstUserMessagePreview,
|
||||||
conversation.snapshot?.firstUserMessagePreview,
|
conversation.snapshot?.firstUserMessagePreview,
|
||||||
conversation.userPreview,
|
conversation.userPreview,
|
||||||
conversation.title,
|
|
||||||
conversation.name,
|
|
||||||
userMessage?.text,
|
userMessage?.text,
|
||||||
userMessage?.content,
|
userMessage?.content,
|
||||||
userMessage?.title
|
userMessage?.title,
|
||||||
|
conversation.title,
|
||||||
|
conversation.name
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...conversation,
|
...conversation,
|
||||||
key: sessionId ?? conversationId,
|
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} 条`,
|
subtitle: `${status === "running" ? "处理中" : trace ? `trace ${shortToken(trace, 8)}` : "未开始"} · ${conversation.messageCount ?? conversation.messages?.length ?? 0} 条`,
|
||||||
sessionId,
|
sessionId,
|
||||||
threadId: firstNonEmptyString(conversation.threadId, conversation.session?.threadId),
|
threadId: firstNonEmptyString(conversation.threadId, conversation.session?.threadId),
|
||||||
@@ -215,31 +215,21 @@ function compareProviderProfileOptions(left: ProviderProfileOption, right: Provi
|
|||||||
return left.value.localeCompare(right.value);
|
return left.value.localeCompare(right.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncateLabel(value: string | null): string | null {
|
function firstReadableSentence(...values: unknown[]): 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 {
|
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
const text = readableText(value);
|
const text = readableSentence(value);
|
||||||
if (text) return text;
|
if (text) return text;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readableText(value: unknown): string | null {
|
function readableSentence(value: unknown): string | null {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") return firstSentenceFromText(value);
|
||||||
const text = value.replace(/\s+/gu, " ").trim();
|
|
||||||
return text && text !== OBJECT_OBJECT_TEXT ? text : null;
|
|
||||||
}
|
|
||||||
if (typeof value === "number" || typeof value === "boolean") return String(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;
|
if (!value || typeof value !== "object") return null;
|
||||||
const record = value as Record<string, unknown>;
|
const record = value as Record<string, unknown>;
|
||||||
return firstReadableText(
|
return firstReadableSentence(
|
||||||
record.text,
|
record.text,
|
||||||
record.content,
|
record.content,
|
||||||
record.message,
|
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 {
|
function shortToken(value: string | null | undefined, size: number): string {
|
||||||
const text = String(value ?? "");
|
const text = String(value ?? "");
|
||||||
if (text.length <= size) return text;
|
if (text.length <= size) return text;
|
||||||
|
|||||||
@@ -606,13 +606,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.session-tab {
|
.session-tab {
|
||||||
display: grid;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 34px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 4px;
|
gap: 10px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
padding: 10px;
|
padding: 7px 9px;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -622,16 +625,27 @@
|
|||||||
background: #f0f9ff;
|
background: #f0f9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tab strong,
|
.session-tab-title {
|
||||||
.session-tab span,
|
min-width: 0;
|
||||||
.session-tab small {
|
flex: 1 1 auto;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tab span,
|
.session-tab-time {
|
||||||
.session-tab small,
|
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 {
|
.muted-box {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user