Merge pull request #1225 from pikasTech/fix/1224-webui
fix: simplify v0.3 workbench diagnostics layout
This commit is contained in:
@@ -10,16 +10,22 @@ const auth = useAuthStore();
|
|||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
|
|
||||||
const navSections = [
|
const navSections = [
|
||||||
{ title: "工作台", items: [{ name: "CodeWorkbench", label: "Code", path: "/workbench" }, { name: "Dashboard", label: "概览", path: "/dashboard" }] },
|
{ title: "工作台", items: [{ name: "CodeWorkbench", label: "Code", path: "/workbench", icon: "C" }, { name: "Dashboard", label: "概览", path: "/dashboard", icon: "D" }] },
|
||||||
{ title: "用户", items: [{ name: "ApiKeys", label: "API Keys", path: "/api-keys" }, { name: "Usage", label: "用量", path: "/usage" }, { name: "Billing", label: "充值", path: "/billing" }] },
|
{ title: "用户", items: [{ name: "ApiKeys", label: "API Keys", path: "/api-keys", icon: "K" }, { name: "Usage", label: "用量", path: "/usage", icon: "U" }, { name: "Billing", label: "充值", path: "/billing", icon: "$" }] },
|
||||||
{ title: "管理", items: [{ name: "Access", label: "授权", path: "/admin/access" }, { name: "Users", label: "用户", path: "/admin/users" }, { name: "AdminBilling", label: "账务", path: "/admin/billing" }, { name: "HwpodGroups", label: "HWPOD", path: "/admin/hwpod-groups" }, { name: "ProviderProfiles", label: "Profiles", path: "/admin/provider-profiles" }] },
|
{ title: "管理", items: [{ name: "Access", label: "授权", path: "/admin/access", icon: "A" }, { name: "Users", label: "用户", path: "/admin/users", icon: "P" }, { name: "AdminBilling", label: "账务", path: "/admin/billing", icon: "B" }, { name: "HwpodGroups", label: "HWPOD", path: "/admin/hwpod-groups", icon: "H" }, { name: "ProviderProfiles", label: "Profiles", path: "/admin/provider-profiles", icon: "R" }] },
|
||||||
{ title: "系统", items: [{ name: "Gate", label: "Gate", path: "/gate" }, { name: "Performance", label: "性能", path: "/performance" }, { name: "Skills", label: "Skills", path: "/skills" }, { name: "Settings", label: "设置", path: "/settings" }, { name: "Help", label: "帮助", path: "/help" }] }
|
{ title: "系统", items: [{ name: "Gate", label: "Gate", path: "/gate", icon: "G" }, { name: "Performance", label: "性能", path: "/performance", icon: "M" }, { name: "Skills", label: "Skills", path: "/skills", icon: "S" }, { name: "Settings", label: "设置", path: "/settings", icon: "T" }, { name: "Help", label: "帮助", path: "/help", icon: "?" }] }
|
||||||
];
|
];
|
||||||
|
|
||||||
const shellless = computed(() => route.name === "Login" || route.name === "Register" || route.name === "NotFound");
|
const shellless = computed(() => route.name === "Login" || route.name === "Register" || route.name === "NotFound");
|
||||||
|
|
||||||
function go(path: string): void {
|
async function go(path: string): Promise<void> {
|
||||||
void router.push(path);
|
if (route.path === path) return;
|
||||||
|
app.setLoading(true);
|
||||||
|
try {
|
||||||
|
await router.push(path);
|
||||||
|
} finally {
|
||||||
|
window.setTimeout(() => app.setLoading(false), 120);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -27,7 +33,7 @@ function go(path: string): void {
|
|||||||
<main v-if="shellless" class="auth-main">
|
<main v-if="shellless" class="auth-main">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
<main v-else class="platform-shell">
|
<main v-else class="platform-shell" :class="{ 'is-sidebar-collapsed': app.sidebarCollapsed }">
|
||||||
<aside class="platform-sidebar" :data-collapsed="app.sidebarCollapsed">
|
<aside class="platform-sidebar" :data-collapsed="app.sidebarCollapsed">
|
||||||
<div class="brand-lockup">
|
<div class="brand-lockup">
|
||||||
<span class="brand-mark">HW</span>
|
<span class="brand-mark">HW</span>
|
||||||
@@ -39,9 +45,9 @@ function go(path: string): void {
|
|||||||
<nav class="nav-groups" aria-label="主导航">
|
<nav class="nav-groups" aria-label="主导航">
|
||||||
<section v-for="section in navSections" :key="section.title" class="nav-section">
|
<section v-for="section in navSections" :key="section.title" class="nav-section">
|
||||||
<h2>{{ section.title }}</h2>
|
<h2>{{ section.title }}</h2>
|
||||||
<button v-for="item in section.items" :key="item.name" class="nav-item" :data-active="route.name === item.name" type="button" @click="go(item.path)">
|
<button v-for="item in section.items" :key="item.name" class="nav-item" :data-active="route.name === item.name" type="button" :title="item.label" :aria-label="item.label" @click="go(item.path)">
|
||||||
<span class="nav-dot" />
|
<span class="nav-icon" aria-hidden="true">{{ item.icon }}</span>
|
||||||
<span>{{ item.label }}</span>
|
<span class="nav-label">{{ item.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -50,6 +56,7 @@ function go(path: string): void {
|
|||||||
<header class="platform-topbar">
|
<header class="platform-topbar">
|
||||||
<button class="icon-button" type="button" aria-label="折叠侧边栏" @click="app.sidebarCollapsed = !app.sidebarCollapsed">☰</button>
|
<button class="icon-button" type="button" aria-label="折叠侧边栏" @click="app.sidebarCollapsed = !app.sidebarCollapsed">☰</button>
|
||||||
<div class="topbar-status">
|
<div class="topbar-status">
|
||||||
|
<span v-if="app.loading" class="topbar-progress" aria-live="polite"><span aria-hidden="true" /></span>
|
||||||
<span>同源 API</span>
|
<span>同源 API</span>
|
||||||
<strong>{{ auth.user?.displayName || auth.user?.username || auth.user?.name || "HWLAB 用户" }}</strong>
|
<strong>{{ auth.user?.displayName || auth.user?.username || auth.user?.name || "HWLAB 用户" }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
import type { ChatMessage } from "@/types";
|
import type { ChatMessage } from "@/types";
|
||||||
import StatusBadge from "@/components/common/StatusBadge.vue";
|
import StatusBadge from "@/components/common/StatusBadge.vue";
|
||||||
import TraceTimeline from "@/components/agent/TraceTimeline.vue";
|
import TraceTimeline from "@/components/agent/TraceTimeline.vue";
|
||||||
@@ -11,6 +12,12 @@ import { traceIdentityText } from "./message-rendering";
|
|||||||
|
|
||||||
const workbench = useWorkbenchStore();
|
const workbench = useWorkbenchStore();
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
const detailMessageId = ref<string | null>(null);
|
||||||
|
const detailMessage = computed(() => workbench.messages.find((message) => message.id === detailMessageId.value) ?? null);
|
||||||
|
|
||||||
|
function openDetails(message: ChatMessage): void {
|
||||||
|
detailMessageId.value = message.id;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -18,22 +25,36 @@ const clipboard = useClipboard();
|
|||||||
<article v-for="message in workbench.messages" :key="message.id" class="message-card" :data-role="message.role" :data-status="message.status">
|
<article v-for="message in workbench.messages" :key="message.id" class="message-card" :data-role="message.role" :data-status="message.status">
|
||||||
<header>
|
<header>
|
||||||
<strong>{{ message.title }}</strong>
|
<strong>{{ message.title }}</strong>
|
||||||
<StatusBadge :status="message.status" />
|
<div class="message-header-actions">
|
||||||
|
<StatusBadge :status="message.status" />
|
||||||
|
<button v-if="message.role === 'agent'" class="message-detail-button" type="button" aria-label="消息详情" title="消息详情" @click="openDetails(message)">i</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<MessageMarkdown class="message-text" :source="message.text" />
|
<MessageMarkdown class="message-text" :source="message.text" />
|
||||||
<TraceTimeline :trace="message.runnerTrace" />
|
|
||||||
<CodeAgentStatusSummary :message="message" />
|
|
||||||
<MessageActions
|
|
||||||
:message="message"
|
|
||||||
@cancel="workbench.cancelAgentMessage"
|
|
||||||
@retry="workbench.retryAgentMessage"
|
|
||||||
@replay-trace="workbench.replayAgentTrace"
|
|
||||||
@copy-trace="(target: ChatMessage) => clipboard.copy(traceIdentityText(target))"
|
|
||||||
/>
|
|
||||||
</article>
|
</article>
|
||||||
<article v-if="workbench.messages.length === 0" class="message-card empty-message">
|
<article v-if="workbench.messages.length === 0" class="message-card empty-message">
|
||||||
<header><strong>等待会话</strong><StatusBadge status="source" label="SOURCE" /></header>
|
<header><strong>等待会话</strong><StatusBadge status="source" label="SOURCE" /></header>
|
||||||
<MessageMarkdown class="message-text" source="请先显式新建或选择 session。Code Agent 不会自动滚动新 session 掩盖失败。" />
|
<MessageMarkdown class="message-text" source="请先显式新建或选择 session。Code Agent 不会自动滚动新 session 掩盖失败。" />
|
||||||
</article>
|
</article>
|
||||||
|
<div v-if="detailMessage" class="workbench-dialog-backdrop" role="presentation" @click.self="detailMessageId = null">
|
||||||
|
<section id="message-detail-dialog" class="workbench-dialog wide" role="dialog" aria-modal="true" aria-labelledby="message-detail-title">
|
||||||
|
<header>
|
||||||
|
<div class="dialog-title-stack">
|
||||||
|
<h2 id="message-detail-title">消息详情</h2>
|
||||||
|
<p>{{ detailMessage.traceId || detailMessage.runnerTrace?.traceId || 'trace 未观测' }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="dialog-close" aria-label="关闭" @click="detailMessageId = null">x</button>
|
||||||
|
</header>
|
||||||
|
<TraceTimeline :trace="detailMessage.runnerTrace" />
|
||||||
|
<CodeAgentStatusSummary :message="detailMessage" />
|
||||||
|
<MessageActions
|
||||||
|
:message="detailMessage"
|
||||||
|
@cancel="workbench.cancelAgentMessage"
|
||||||
|
@retry="workbench.retryAgentMessage"
|
||||||
|
@replay-trace="workbench.replayAgentTrace"
|
||||||
|
@copy-trace="(target: ChatMessage) => clipboard.copy(traceIdentityText(target))"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -21,13 +21,14 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
|||||||
const gatewayShellTimeoutMs = ref(readNumber("hwlab.workbench.gatewayShellTimeoutMs.v1", DEFAULT_GATEWAY_TIMEOUT_MS));
|
const gatewayShellTimeoutMs = ref(readNumber("hwlab.workbench.gatewayShellTimeoutMs.v1", DEFAULT_GATEWAY_TIMEOUT_MS));
|
||||||
const live = ref<LiveSurface | null>(null);
|
const live = ref<LiveSurface | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const switchingConversationId = ref<string | null>(null);
|
||||||
const chatPending = ref(false);
|
const chatPending = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const activityRef = ref({ lastActivityAt: Date.now(), lastActivityIso: new Date().toISOString(), waitingFor: "idle", lastEventLabel: null as string | null });
|
const activityRef = ref({ lastActivityAt: Date.now(), lastActivityIso: new Date().toISOString(), waitingFor: "idle", lastEventLabel: null as string | null });
|
||||||
const activeAbort = ref<AbortController | null>(null);
|
const activeAbort = ref<AbortController | null>(null);
|
||||||
const currentRequest = ref<{ traceId: string; conversationId: string | null; sessionId: string | null; threadId: string | null; status?: string | null } | null>(null);
|
const currentRequest = ref<{ traceId: string; conversationId: string | null; sessionId: string | null; threadId: string | null; status?: string | null } | null>(null);
|
||||||
|
|
||||||
const activeConversationId = computed(() => firstNonEmptyString(workspace.value?.selectedConversationId, workspace.value?.workspace?.selectedConversationId, messages.value.find((message) => message.conversationId)?.conversationId));
|
const activeConversationId = computed(() => firstNonEmptyString(switchingConversationId.value, workspace.value?.selectedConversationId, workspace.value?.workspace?.selectedConversationId, messages.value.find((message) => message.conversationId)?.conversationId));
|
||||||
const selectedSessionId = computed(() => firstNonEmptyString(workspace.value?.selectedAgentSessionId, workspace.value?.workspace?.selectedAgentSessionId, activeConversation.value?.sessionId));
|
const selectedSessionId = computed(() => firstNonEmptyString(workspace.value?.selectedAgentSessionId, workspace.value?.workspace?.selectedAgentSessionId, activeConversation.value?.sessionId));
|
||||||
const selectedThreadId = computed(() => firstNonEmptyString(workspace.value?.workspace?.threadId, activeConversation.value?.threadId));
|
const selectedThreadId = computed(() => firstNonEmptyString(workspace.value?.workspace?.threadId, activeConversation.value?.threadId));
|
||||||
const activeConversation = computed(() => conversations.value.find((item) => item.conversationId === activeConversationId.value) ?? null);
|
const activeConversation = computed(() => conversations.value.find((item) => item.conversationId === activeConversationId.value) ?? null);
|
||||||
@@ -75,7 +76,9 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
|||||||
async function createSession(): Promise<void> {
|
async function createSession(): Promise<void> {
|
||||||
const current = await ensureWorkspace();
|
const current = await ensureWorkspace();
|
||||||
if (!current) return;
|
if (!current) return;
|
||||||
|
loading.value = true;
|
||||||
const response = await api.workbench.selectConversation(current.workspaceId, { projectId: activeProjectId.value, create: true, updatedByClient: "cloud-web-vue" });
|
const response = await api.workbench.selectConversation(current.workspaceId, { projectId: activeProjectId.value, create: true, updatedByClient: "cloud-web-vue" });
|
||||||
|
loading.value = false;
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
workspace.value = response.data?.workspace ?? current;
|
workspace.value = response.data?.workspace ?? current;
|
||||||
messages.value = messagesFromWorkspace(workspace.value);
|
messages.value = messagesFromWorkspace(workspace.value);
|
||||||
@@ -87,12 +90,24 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
|||||||
async function selectConversation(conversation: ConversationRecord): Promise<void> {
|
async function selectConversation(conversation: ConversationRecord): Promise<void> {
|
||||||
const current = await ensureWorkspace();
|
const current = await ensureWorkspace();
|
||||||
if (!current) return;
|
if (!current) return;
|
||||||
|
const previousWorkspace = workspace.value;
|
||||||
|
const previousMessages = messages.value;
|
||||||
const tabProjectId = conversation.projectId ?? activeProjectId.value;
|
const tabProjectId = conversation.projectId ?? activeProjectId.value;
|
||||||
|
switchingConversationId.value = conversation.conversationId;
|
||||||
|
workspace.value = optimisticWorkspaceSelection(current, conversation, tabProjectId);
|
||||||
|
messages.value = messagesFromConversation(conversation);
|
||||||
|
loading.value = true;
|
||||||
const response = await api.workbench.selectConversation(current.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue" });
|
const response = await api.workbench.selectConversation(current.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue" });
|
||||||
|
loading.value = false;
|
||||||
|
switchingConversationId.value = null;
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
workspace.value = response.data?.workspace ?? current;
|
workspace.value = response.data?.workspace ?? current;
|
||||||
messages.value = messagesFromWorkspace(workspace.value);
|
messages.value = messagesFromWorkspace(workspace.value);
|
||||||
currentRequest.value = null;
|
currentRequest.value = null;
|
||||||
|
} else {
|
||||||
|
workspace.value = previousWorkspace;
|
||||||
|
messages.value = previousMessages;
|
||||||
|
error.value = response.error ?? "session switch failed";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +329,28 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
|||||||
return { projectId, workspace, conversations, messages, providerProfile, providerOptions, recentDrafts, codeAgentTimeoutMs, gatewayShellTimeoutMs, live, loading, chatPending, error, activeConversationId, selectedSessionId, selectedThreadId, sessionTabs, composer, hydrate, refreshLive, createSession, selectConversation, deleteCurrentSession, refreshConversations, submitMessage, cancelAgentMessage, cancelRunningTrace, retryAgentMessage, replayAgentTrace, setProviderProfile, refreshProviderOptions, pickDraft, clearRecentDrafts, clearConversation, recordActivity };
|
return { projectId, workspace, conversations, messages, providerProfile, providerOptions, recentDrafts, codeAgentTimeoutMs, gatewayShellTimeoutMs, live, loading, chatPending, error, activeConversationId, selectedSessionId, selectedThreadId, sessionTabs, composer, hydrate, refreshLive, createSession, selectConversation, deleteCurrentSession, refreshConversations, submitMessage, cancelAgentMessage, cancelRunningTrace, retryAgentMessage, replayAgentTrace, setProviderProfile, refreshProviderOptions, pickDraft, clearRecentDrafts, clearConversation, recordActivity };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function optimisticWorkspaceSelection(current: WorkspaceRecord, conversation: ConversationRecord, projectId: string): WorkspaceRecord {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
projectId,
|
||||||
|
selectedConversationId: conversation.conversationId,
|
||||||
|
selectedAgentSessionId: conversation.sessionId ?? current.selectedAgentSessionId,
|
||||||
|
selectedConversation: conversation,
|
||||||
|
workspace: {
|
||||||
|
...(current.workspace ?? {}),
|
||||||
|
projectId,
|
||||||
|
selectedConversationId: conversation.conversationId,
|
||||||
|
selectedAgentSessionId: conversation.sessionId ?? current.workspace?.selectedAgentSessionId,
|
||||||
|
threadId: conversation.threadId ?? current.workspace?.threadId,
|
||||||
|
messages: conversation.messages ?? []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function messagesFromConversation(conversation: ConversationRecord): ChatMessage[] {
|
||||||
|
return (conversation.messages ?? []).map((message) => ({ ...message, id: message.id ?? nextProtocolId("msg"), title: message.title ?? (message.role === "user" ? "用户" : "Code Agent"), createdAt: message.createdAt ?? new Date().toISOString(), status: message.status ?? "source" }));
|
||||||
|
}
|
||||||
|
|
||||||
function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[] {
|
function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[] {
|
||||||
const selected = workspace?.selectedConversation?.messages;
|
const selected = workspace?.selectedConversation?.messages;
|
||||||
const embedded = workspace?.workspace?.messages;
|
const embedded = workspace?.workspace?.messages;
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
.platform-shell {
|
.platform-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
height: 100dvh;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
grid-template-columns: 248px minmax(0, 1fr);
|
grid-template-columns: 248px minmax(0, 1fr);
|
||||||
|
overflow: hidden;
|
||||||
background: #eef3f8;
|
background: #eef3f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.platform-shell.is-sidebar-collapsed {
|
||||||
|
grid-template-columns: 76px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.platform-sidebar {
|
.platform-sidebar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -18,13 +24,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.platform-sidebar[data-collapsed="true"] {
|
.platform-sidebar[data-collapsed="true"] {
|
||||||
width: 72px;
|
width: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-main {
|
.platform-main {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
grid-template-rows: 58px minmax(0, 1fr);
|
grid-template-rows: 58px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +49,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.platform-content {
|
.platform-content {
|
||||||
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.platform-content > * {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.brand-lockup {
|
.brand-lockup {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -91,6 +108,18 @@
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-stack {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-route {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-section h2 {
|
.nav-section h2 {
|
||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
@@ -113,6 +142,36 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item[data-active="true"] .nav-icon {
|
||||||
|
background: #bae6fd;
|
||||||
|
color: #075985;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-sidebar[data-collapsed="true"] .brand-lockup div,
|
||||||
|
.platform-sidebar[data-collapsed="true"] .nav-section h2,
|
||||||
|
.platform-sidebar[data-collapsed="true"] .nav-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-sidebar[data-collapsed="true"] .nav-item {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-item[data-active="true"] {
|
.nav-item[data-active="true"] {
|
||||||
background: #e0f2fe;
|
background: #e0f2fe;
|
||||||
color: #0c4a6e;
|
color: #0c4a6e;
|
||||||
@@ -139,6 +198,27 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topbar-progress {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-progress span {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid #bae6fd;
|
||||||
|
border-top-color: #0e7490;
|
||||||
|
border-radius: 999px;
|
||||||
|
animation: workbench-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes workbench-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
.auth-main,
|
.auth-main,
|
||||||
.login-shell {
|
.login-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -190,22 +270,81 @@
|
|||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-status-row {
|
.workbench-diagnostics-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
min-height: 34px;
|
||||||
gap: 10px;
|
|
||||||
color: #475569;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-live-strip {
|
.workbench-diagnostics-toggle {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
min-height: 34px;
|
||||||
align-items: stretch;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
color: #334155;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-diagnostics-toggle > span:first-child,
|
||||||
|
.message-detail-button {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e0f2fe;
|
||||||
|
color: #0c4a6e;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-diagnostics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-diagnostics-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-diagnostics-summary div {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-diagnostics-summary dt,
|
||||||
|
.workbench-diagnostics-summary dd {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-diagnostics-summary dt {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-diagnostics-summary dd {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
.probe-card,
|
.probe-card,
|
||||||
.live-build-summary {
|
.live-build-summary {
|
||||||
min-width: min(320px, 100%);
|
min-width: min(320px, 100%);
|
||||||
@@ -373,8 +512,9 @@
|
|||||||
|
|
||||||
.workbench-grid {
|
.workbench-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: calc(100vh - 180px);
|
min-height: 0;
|
||||||
grid-template-columns: minmax(220px, 300px) minmax(0, 1fr) minmax(280px, 360px);
|
height: 100%;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) minmax(280px, 360px);
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,6 +533,8 @@
|
|||||||
|
|
||||||
.workbench-center {
|
.workbench-center {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
grid-template-rows: minmax(0, 1fr) auto;
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -402,6 +544,8 @@
|
|||||||
.hwpod-panel {
|
.hwpod-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -409,6 +553,14 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-rail {
|
||||||
|
grid-template-rows: auto auto minmax(0, 1fr) auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hwpod-panel {
|
||||||
|
grid-template-rows: auto auto minmax(0, auto) auto minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -424,6 +576,7 @@
|
|||||||
.session-list,
|
.session-list,
|
||||||
.conversation-panel {
|
.conversation-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
@@ -488,6 +641,19 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-header-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-detail-button {
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.message-text,
|
.message-text,
|
||||||
.message-markdown {
|
.message-markdown {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -865,9 +1031,18 @@
|
|||||||
.hwpod-node-status,
|
.hwpod-node-status,
|
||||||
.hwpod-event-panel {
|
.hwpod-event-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hwpod-node-status {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hwpod-event-panel {
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.hwpod-node-summary {
|
.hwpod-node-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -971,7 +1146,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hwpod-event-scroll {
|
.hwpod-event-scroll {
|
||||||
max-height: 210px;
|
min-height: 0;
|
||||||
|
max-height: none;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid #d8e1eb;
|
border: 1px solid #d8e1eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -1686,7 +1862,7 @@
|
|||||||
|
|
||||||
.platform-sidebar .brand-lockup div,
|
.platform-sidebar .brand-lockup div,
|
||||||
.nav-section h2,
|
.nav-section h2,
|
||||||
.nav-item span:not(.nav-dot) {
|
.nav-label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1701,6 +1877,22 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-route {
|
||||||
|
height: auto;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-grid,
|
||||||
|
.session-rail,
|
||||||
|
.workbench-center,
|
||||||
|
.hwpod-panel {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hwpod-event-scroll {
|
||||||
|
max-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
.field-grid,
|
.field-grid,
|
||||||
.form-grid.two-col,
|
.form-grid.two-col,
|
||||||
.compact-form-grid {
|
.compact-form-grid {
|
||||||
@@ -1711,6 +1903,8 @@
|
|||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.platform-shell {
|
.platform-shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
height: auto;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-sidebar {
|
.platform-sidebar {
|
||||||
@@ -1724,6 +1918,7 @@
|
|||||||
|
|
||||||
.platform-content {
|
.platform-content {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-topbar {
|
.platform-topbar {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import PageHeader from "@/components/common/PageHeader.vue";
|
|
||||||
import StatusBadge from "@/components/common/StatusBadge.vue";
|
import StatusBadge from "@/components/common/StatusBadge.vue";
|
||||||
import CommandComposer from "@/components/workbench/CommandComposer.vue";
|
import CommandComposer from "@/components/workbench/CommandComposer.vue";
|
||||||
import ConversationPanel from "@/components/workbench/ConversationPanel.vue";
|
import ConversationPanel from "@/components/workbench/ConversationPanel.vue";
|
||||||
@@ -12,22 +11,18 @@ import { useAutoRefresh } from "@/composables/useAutoRefresh";
|
|||||||
import { useWorkbenchStore } from "@/stores/workbench";
|
import { useWorkbenchStore } from "@/stores/workbench";
|
||||||
|
|
||||||
const workbench = useWorkbenchStore();
|
const workbench = useWorkbenchStore();
|
||||||
|
const diagnosticsOpen = ref(false);
|
||||||
onMounted(() => void workbench.hydrate());
|
onMounted(() => void workbench.hydrate());
|
||||||
useAutoRefresh(() => workbench.refreshLive(), 30_000);
|
useAutoRefresh(() => workbench.refreshLive(), 30_000);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section id="workspace" class="workbench-route">
|
<section id="workspace" class="workbench-route">
|
||||||
<PageHeader eyebrow="Workbench" title="Code Workbench" description="Code Agent 是平台控制台中的一个应用页面;session、trace、HWPOD evidence 仍走 Cloud Web 同源 API。" />
|
<div class="workbench-diagnostics-bar">
|
||||||
<div class="workbench-status-row">
|
<button id="workbench-diagnostics-toggle" class="workbench-diagnostics-toggle" type="button" aria-haspopup="dialog" @click="diagnosticsOpen = true">
|
||||||
<StatusBadge :status="workbench.chatPending ? 'running' : 'source'" :label="workbench.chatPending ? '处理中' : 'SOURCE'" />
|
<span aria-hidden="true">i</span>
|
||||||
<span>Provider: {{ workbench.providerProfile }}</span>
|
<StatusBadge :status="workbench.chatPending ? 'running' : 'source'" :label="workbench.chatPending ? '处理中' : '诊断'" />
|
||||||
<span>Composer: {{ workbench.composer.submitMode }}{{ workbench.composer.targetTraceId ? ` ${workbench.composer.targetTraceId}` : '' }}</span>
|
</button>
|
||||||
<span>Timeout: inactivity {{ Math.round(workbench.codeAgentTimeoutMs / 1000) }}s</span>
|
|
||||||
</div>
|
|
||||||
<div class="workbench-live-strip" aria-label="Workbench live status">
|
|
||||||
<WorkbenchProbePanel :live="workbench.live" />
|
|
||||||
<WorkbenchBuildSummary :live="workbench.live" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="workbench-grid">
|
<div class="workbench-grid">
|
||||||
<SessionRail />
|
<SessionRail />
|
||||||
@@ -37,5 +32,26 @@ useAutoRefresh(() => workbench.refreshLive(), 30_000);
|
|||||||
</main>
|
</main>
|
||||||
<HwpodNodeOpsPanel />
|
<HwpodNodeOpsPanel />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="diagnosticsOpen" class="workbench-dialog-backdrop" role="presentation" @click.self="diagnosticsOpen = false">
|
||||||
|
<section id="workbench-diagnostics-dialog" class="workbench-dialog wide" role="dialog" aria-modal="true" aria-labelledby="workbench-diagnostics-title">
|
||||||
|
<header>
|
||||||
|
<div class="dialog-title-stack">
|
||||||
|
<h2 id="workbench-diagnostics-title">Workbench 诊断</h2>
|
||||||
|
<p>Code Agent session、trace、HWPOD evidence 仍走 Cloud Web 同源 API。</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="dialog-close" aria-label="关闭" @click="diagnosticsOpen = false">x</button>
|
||||||
|
</header>
|
||||||
|
<div class="workbench-diagnostics-grid">
|
||||||
|
<dl class="workbench-diagnostics-summary">
|
||||||
|
<div><dt>Provider</dt><dd>{{ workbench.providerProfile }}</dd></div>
|
||||||
|
<div><dt>Composer</dt><dd>{{ workbench.composer.submitMode }}{{ workbench.composer.targetTraceId ? ` ${workbench.composer.targetTraceId}` : '' }}</dd></div>
|
||||||
|
<div><dt>Timeout</dt><dd>inactivity {{ Math.round(workbench.codeAgentTimeoutMs / 1000) }}s</dd></div>
|
||||||
|
<div><dt>Session</dt><dd>{{ workbench.selectedSessionId || '未选择' }}</dd></div>
|
||||||
|
</dl>
|
||||||
|
<WorkbenchProbePanel :live="workbench.live" />
|
||||||
|
<WorkbenchBuildSummary :live="workbench.live" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user