Commit 5e8d2c51 authored by AI-甘富林's avatar AI-甘富林

Refine desktop chat interface layout

parent e74cd2e9
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import type { import type {
AppConfig, AppConfig,
ChatLaunchState, ChatLaunchState,
...@@ -20,7 +21,7 @@ import type { ...@@ -20,7 +21,7 @@ import type {
WorkspaceSummary WorkspaceSummary
} from "@qjclaw/shared-types"; } from "@qjclaw/shared-types";
type ViewMode = "chat" | "skills" | "plugins" | "settings"; type ViewMode = "chat" | "experts" | "plugins" | "settings";
type Tone = "positive" | "warning"; type Tone = "positive" | "warning";
type MessageStreamState = "streaming" | "error"; type MessageStreamState = "streaming" | "error";
type SendPhase = "idle" | "preparing" | "streaming" | "finalizing"; type SendPhase = "idle" | "preparing" | "streaming" | "finalizing";
...@@ -150,6 +151,36 @@ function buildAssistantPlaceholder(statusLabel: string): UiChatMessage { ...@@ -150,6 +151,36 @@ function buildAssistantPlaceholder(statusLabel: string): UiChatMessage {
}; };
} }
function LobsterClawIcon() {
return (
<svg viewBox="0 0 64 64" aria-hidden="true" focusable="false">
<defs>
<linearGradient id="lobster-claw-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#ff9a6b" />
<stop offset="100%" stopColor="#ff7b76" />
</linearGradient>
</defs>
<path
d="M14 42c0-7.8 6.3-14 14-14h3.5c2.2 0 4.3.8 5.9 2.2L50 42.8c1 .9 1.1 2.4.2 3.4l-3.2 3.7a2.4 2.4 0 0 1-3.4.2L31 39.5H28c-1.9 0-3.5 1.6-3.5 3.5v4.2c0 1.3-1.1 2.3-2.3 2.3H16.3c-1.3 0-2.3-1-2.3-2.3V42Z"
fill="url(#lobster-claw-gradient)"
/>
<path
d="M38 16.5c6.7 0 12.3 5.1 13 11.8l.1 1.4-5.9-.1c-5.6 0-10.6-3.3-12.7-8.4l-1.2-2.8 2.9-.9c1.3-.4 2.5-.7 3.8-.9Zm-3.6 14.1c3.7 0 7.2 1.5 9.7 4.2l1.1 1.2-5.3 2.7a13.9 13.9 0 0 1-17.6-4.6l-1.8-2.5 2.6-.9c3.6-1.3 7.5-2 11.3-2Z"
fill="#fff3eb"
opacity="0.95"
/>
<path
d="M31.3 22.8c2.2 5.4 7.5 8.9 13.3 8.9h6.5M22.1 32.7c3 3.9 7.6 6.2 12.5 6.2 2.2 0 4.4-.4 6.5-1.3"
fill="none"
stroke="#c85650"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.2"
/>
</svg>
);
}
const DEFAULT_SKILL = { const DEFAULT_SKILL = {
id: "default-chat", id: "default-chat",
name: "\u9ed8\u8ba4\u5bf9\u8bdd", name: "\u9ed8\u8ba4\u5bf9\u8bdd",
...@@ -166,7 +197,8 @@ const ui = { ...@@ -166,7 +197,8 @@ const ui = {
appDesc: "\u7ed1\u5b9a api_key \u540e\u81ea\u52a8\u540c\u6b65\u8fd0\u884c\u65f6\u914d\u7f6e\u3002", appDesc: "\u7ed1\u5b9a api_key \u540e\u81ea\u52a8\u540c\u6b65\u8fd0\u884c\u65f6\u914d\u7f6e\u3002",
heroLine: "\u5343\u5320Claw\uff0c\u60a8\u8eab\u8fb9\u6700\u5f97\u529b\u7684\u5458\u5de5\uff0cStart Your Ideas.", heroLine: "\u5343\u5320Claw\uff0c\u60a8\u8eab\u8fb9\u6700\u5f97\u529b\u7684\u5458\u5de5\uff0cStart Your Ideas.",
chat: "\u5bf9\u8bdd", chat: "\u5bf9\u8bdd",
skills: "\u6280\u80fd", skills: "\u80fd\u529b",
experts: "\u4e13\u5bb6",
plugins: "\u63d2\u4ef6", plugins: "\u63d2\u4ef6",
settings: "\u8bbe\u7f6e", settings: "\u8bbe\u7f6e",
bound: "\u5df2\u7ed1\u5b9a", bound: "\u5df2\u7ed1\u5b9a",
...@@ -179,11 +211,16 @@ const ui = { ...@@ -179,11 +211,16 @@ const ui = {
bindNow: "\u7acb\u5373\u7ed1\u5b9a", bindNow: "\u7acb\u5373\u7ed1\u5b9a",
binding: "\u7ed1\u5b9a\u4e2d...", binding: "\u7ed1\u5b9a\u4e2d...",
changeApiKey: "\u66f4\u6362\u5458\u5de5\u5bc6\u94a5", changeApiKey: "\u66f4\u6362\u5458\u5de5\u5bc6\u94a5",
skillChoice: "\u9009\u62e9\u6280\u80fd", skillChoice: "\u9009\u62e9\u80fd\u529b",
clearSkill: "\u6e05\u7a7a\u6280\u80fd", clearSkill: "\u6e05\u7a7a\u80fd\u529b",
skillMenuTitle: "\u9009\u62e9\u6280\u80fd", skillMenuTitle: "\u9009\u62e9\u80fd\u529b",
noMessages: "\u5f53\u524d\u6ca1\u6709\u6d88\u606f\uff0c\u8bf7\u5148\u53d1\u9001\u4e00\u6761\u6d88\u606f\u3002", noMessages: "\u5f53\u524d\u6ca1\u6709\u6d88\u606f\uff0c\u8bf7\u5148\u53d1\u9001\u4e00\u6761\u6d88\u606f\u3002",
taskPlaceholder: "\u8f93\u5165\u6d88\u606f\u540e\u56de\u8f66\u6216\u70b9\u51fb\u53d1\u9001\u3002", expertSessionsTitle: "\u4f1a\u8bdd",
expertCapabilitiesTitle: "\u80fd\u529b",
expertReady: "\u5df2\u5c31\u7eea",
activeExpert: "\u5f53\u524d\u4e13\u5bb6",
starterQuestionsHint: "\u70b9\u51fb\u95ee\u9898\u540e\u4f1a\u5148\u586b\u5165\u8f93\u5165\u6846\uff0c\u4f60\u53ef\u4ee5\u7ee7\u7eed\u8865\u5145\u540e\u518d\u53d1\u9001\u3002",
taskPlaceholder: "\u8f93\u5165\u4f60\u7684\u9700\u6c42\uff0c\u53ef\u7528 Ctrl+Enter \u53d1\u9001\u3002",
taskDisabledPlaceholder: "\u8bf7\u5148\u7ed1\u5b9a\u5458\u5de5\u5bc6\u94a5\u540e\u5f00\u59cb\u5bf9\u8bdd\u3002", taskDisabledPlaceholder: "\u8bf7\u5148\u7ed1\u5b9a\u5458\u5de5\u5bc6\u94a5\u540e\u5f00\u59cb\u5bf9\u8bdd\u3002",
send: "\u53d1\u9001", send: "\u53d1\u9001",
sending: "\u53d1\u9001\u4e2d...", sending: "\u53d1\u9001\u4e2d...",
...@@ -234,13 +271,13 @@ const ui = { ...@@ -234,13 +271,13 @@ const ui = {
bindFirstError: "\u8bf7\u5148\u7ed1\u5b9a\u5458\u5de5\u5bc6\u94a5\u540e\u518d\u53d1\u9001\u6d88\u606f\u3002", bindFirstError: "\u8bf7\u5148\u7ed1\u5b9a\u5458\u5de5\u5bc6\u94a5\u540e\u518d\u53d1\u9001\u6d88\u606f\u3002",
startingHint: "\u6b63\u5728\u51c6\u5907\u8fd0\u884c\u73af\u5883\uff0c\u8bf7\u7a0d\u5019\u3002", startingHint: "\u6b63\u5728\u51c6\u5907\u8fd0\u884c\u73af\u5883\uff0c\u8bf7\u7a0d\u5019\u3002",
chatNotReadyError: "\u5f53\u524d\u804a\u5929\u6682\u4e0d\u53ef\u7528\uff0c\u8bf7\u68c0\u67e5\u8fd0\u884c\u65f6\u72b6\u6001\u3002", chatNotReadyError: "\u5f53\u524d\u804a\u5929\u6682\u4e0d\u53ef\u7528\uff0c\u8bf7\u68c0\u67e5\u8fd0\u884c\u65f6\u72b6\u6001\u3002",
noSkillCards: "\u5f53\u524d\u6ca1\u6709\u53ef\u7528\u6280\u80fd\u3002", noSkillCards: "\u5f53\u524d\u4e13\u5bb6\u8fd8\u6ca1\u6709\u53ef\u7528\u80fd\u529b\u3002",
pluginTitle: "\u63d2\u4ef6\u5217\u8868", pluginTitle: "\u63d2\u4ef6\u5217\u8868",
noPlugins: "\u5f53\u524d\u6ca1\u6709\u53ef\u7528\u63d2\u4ef6\u3002", noPlugins: "\u5f53\u524d\u6ca1\u6709\u53ef\u7528\u63d2\u4ef6\u3002",
settingsTitle: "\u8bbe\u7f6e", settingsTitle: "\u8bbe\u7f6e",
settingsDesc: "\u914d\u7f6e\u8fd0\u884c\u65f6\u3001\u5bc6\u94a5\u548c\u5de5\u4f5c\u76ee\u5f55\u3002", settingsDesc: "\u914d\u7f6e\u8fd0\u884c\u65f6\u3001\u5bc6\u94a5\u548c\u5de5\u4f5c\u76ee\u5f55\u3002",
chatPageDesc: "\u5728\u8fd9\u91cc\u4e0e\u5343\u5320Claw\u5b8c\u6210\u5bf9\u8bdd\u548c\u534f\u4f5c\u3002", chatPageDesc: "\u5728\u8fd9\u91cc\u4e0e\u4e13\u5bb6\u5b8c\u6210\u5bf9\u8bdd\u548c\u534f\u4f5c\u3002",
skillsPageDesc: "\u67e5\u770b\u5f53\u524d\u5df2\u51c6\u5907\u597d\u7684\u6280\u80fd\u80fd\u529b\u3002", skillsPageDesc: "\u67e5\u770b\u5f53\u524d\u4e13\u5bb6\u5df2\u51c6\u5907\u597d\u7684\u80fd\u529b\u3002",
pluginsPageDesc: "\u67e5\u770b\u5f53\u524d\u5df2\u542f\u7528\u7684\u63d2\u4ef6\u80fd\u529b\u3002", pluginsPageDesc: "\u67e5\u770b\u5f53\u524d\u5df2\u542f\u7528\u7684\u63d2\u4ef6\u80fd\u529b\u3002",
workspacePath: "\u5de5\u4f5c\u76ee\u5f55", workspacePath: "\u5de5\u4f5c\u76ee\u5f55",
save: "\u4fdd\u5b58", save: "\u4fdd\u5b58",
...@@ -266,6 +303,23 @@ const startupCurtainCopy = { ...@@ -266,6 +303,23 @@ const startupCurtainCopy = {
retryHint: "\u51c6\u5907\u5931\u8d25\u540e\u53ef\u91cd\u65b0\u5c1d\u8bd5\uff0c\u6216\u524d\u5f80\u8bbe\u7f6e\u68c0\u67e5\u5bc6\u94a5\u4e0e\u7f51\u7edc\u3002" retryHint: "\u51c6\u5907\u5931\u8d25\u540e\u53ef\u91cd\u65b0\u5c1d\u8bd5\uff0c\u6216\u524d\u5f80\u8bbe\u7f6e\u68c0\u67e5\u5bc6\u94a5\u4e0e\u7f51\u7edc\u3002"
} as const; } as const;
const homeChatCopy = {
title: "首页对话",
microcopy: "Start your idea with clarity.",
emptyTitle: "先说目标,再开始协作。",
emptyDesc: "从一条清晰的问题开始,消息会随着内容自然展开,按需继续补充上下文。",
prompts: [
"帮我把这个需求拆成可执行步骤,并告诉我先做什么。",
"根据我的目标,推荐该优先使用哪类 skill 来完成任务。",
"把这段零散想法整理成一份清晰的执行方案。"
]
} as const;
const expertsPageCopy = {
title: "专家页",
noExperts: "当前还没有可用专家,先在首页直接对话即可。"
} as const;
const mockChatStreamListeners = new Set<ChatStreamListener>(); const mockChatStreamListeners = new Set<ChatStreamListener>();
function emitMockChatStreamEvent(event: ChatStreamEvent) { function emitMockChatStreamEvent(event: ChatStreamEvent) {
...@@ -289,6 +343,16 @@ const pluginDisplayMap: Record<string, { name: string; description: string }> = ...@@ -289,6 +343,16 @@ const pluginDisplayMap: Record<string, { name: string; description: string }> =
}; };
const mockProjects: WorkspaceSummary["projects"] = [
{ id: "xiaohongshu", name: "openclaw-xiaohongshu-skills-delivery", displayName: "\u5c0f\u7ea2\u4e66\u4e13\u5bb6", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "xiaohongshu" },
{ id: "douyin", name: "openclaw-douyin-skills-delivery", displayName: "\u6296\u97f3\u4e13\u5bb6", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "douyin" },
{ id: "browser-ops", name: "???", displayName: "\u6d4f\u89c8\u5668\u81ea\u52a8\u5316\u4e13\u5bb6", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 3, ready: true, platform: "browser" }
];
const mockSessions = [
{ id: "project:xiaohongshu:default", projectId: "xiaohongshu", title: "\u9009\u9898\u8ba8\u8bba", updatedAt: new Date().toISOString() },
{ id: "project:xiaohongshu:publish", projectId: "xiaohongshu", title: "\u53d1\u5e03\u65f6\u95f4\u5b89\u6392", updatedAt: new Date().toISOString() }
];
const mockDesktopApi = { const mockDesktopApi = {
workspace: { workspace: {
getSummary: async () => ({ getSummary: async () => ({
...@@ -311,6 +375,13 @@ const mockDesktopApi = { ...@@ -311,6 +375,13 @@ const mockDesktopApi = {
runtimeCloudState: "ready", runtimeCloudState: "ready",
runtimeState: "running", runtimeState: "running",
runtimeMessage: "mock", runtimeMessage: "mock",
currentProjectId: mockProjects[0].id,
currentProjectName: mockProjects[0].displayName,
projectVersion: mockProjects[0].version,
projectReady: mockProjects[0].ready,
projectCount: mockProjects.length,
projects: mockProjects,
sessions: mockSessions,
skillCount: 2, skillCount: 2,
skills: [ skills: [
{ id: "sheet", name: "Spreadsheet Tools", description: "Process spreadsheets and data summaries.", category: "office", enabled: true, ready: true, downloadState: "ready", fileName: "sheet.md" }, { id: "sheet", name: "Spreadsheet Tools", description: "Process spreadsheets and data summaries.", category: "office", enabled: true, ready: true, downloadState: "ready", fileName: "sheet.md" },
...@@ -356,19 +427,16 @@ const mockDesktopApi = { ...@@ -356,19 +427,16 @@ const mockDesktopApi = {
credits: { getSummary: async () => ({ balance: 0, granted: 0, used: 0, currency: "credits", status: "ok", updatedAt: new Date().toISOString() }) }, credits: { getSummary: async () => ({ balance: 0, granted: 0, used: 0, currency: "credits", status: "ok", updatedAt: new Date().toISOString() }) },
skills: { list: async () => [] }, skills: { list: async () => [] },
projects: { projects: {
list: async () => ([ list: async () => mockProjects,
{ id: "xiaohongshu", name: "\u5c0f\u7ea2\u4e66", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true },
{ id: "douyin", name: "\u6296\u97f3", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 1, ready: true }
]),
setActive: async () => mockDesktopApi.workspace.getSummary() setActive: async () => mockDesktopApi.workspace.getSummary()
}, },
modelConfig: { getSummary: async () => ({ source: "cloud", updatedAt: new Date().toISOString(), fetchedAt: new Date().toISOString(), routingMode: "platform-managed", fallbackMode: "cloud-required", defaultChatModelId: "gpt-5.4-mini", defaultChatModelLabel: "GPT-5.4 Mini", items: [], skillBindings: [], message: "mock" }) }, modelConfig: { getSummary: async () => ({ source: "cloud", updatedAt: new Date().toISOString(), fetchedAt: new Date().toISOString(), routingMode: "platform-managed", fallbackMode: "cloud-required", defaultChatModelId: "gpt-5.4-mini", defaultChatModelLabel: "GPT-5.4 Mini", items: [], skillBindings: [], message: "mock" }) },
system: { getSummary: async () => ({ appName: "QianjiangClaw", appVersion: "0.1.0", isPackaged: false, platform: "win32", arch: "x64", appPath: "D:/qjclaw/apps/desktop", resourcesPath: "D:/qjclaw/apps/desktop/dist", userDataPath: "D:/qjclaw/.tmp/user-data", logsPath: "D:/qjclaw/.tmp/logs" }) }, system: { getSummary: async () => ({ appName: "QianjiangClaw", appVersion: "0.1.0", isPackaged: false, platform: "win32", arch: "x64", appPath: "D:/qjclaw/apps/desktop", resourcesPath: "D:/qjclaw/apps/desktop/dist", userDataPath: "D:/qjclaw/.tmp/user-data", logsPath: "D:/qjclaw/.tmp/logs" }) },
chat: { chat: {
listSessions: async () => ([{ id: "project:xiaohongshu:default", projectId: "xiaohongshu", title: ui.defaultChat, updatedAt: new Date().toISOString() }]), listSessions: async () => mockSessions,
createSession: async (title?: string) => ({ id: `project:xiaohongshu:${createClientMessageId("session")}`, projectId: "xiaohongshu", title: title || "\u65b0\u5bf9\u8bdd", updatedAt: new Date().toISOString() }), createSession: async (title?: string) => ({ id: `project:xiaohongshu:${createClientMessageId("session")}`, projectId: "xiaohongshu", title: title || "\u65b0\u5bf9\u8bdd", updatedAt: new Date().toISOString() }),
closeSession: async () => ([{ id: "project:xiaohongshu:default", projectId: "xiaohongshu", title: ui.defaultChat, updatedAt: new Date().toISOString() }]), closeSession: async () => mockSessions,
listMessages: async () => [{ id: "message-1", role: "assistant", content: "Mock UI active.", createdAt: new Date().toISOString() }], listMessages: async () => [],
sendPrompt: async (sessionId: string, prompt: string, skillId?: string) => ({ sessionId: sessionId || "project:xiaohongshu:default", reply: { id: "reply-1", role: "assistant", content: "Mock: " + prompt, createdAt: new Date().toISOString() }, executionPolicy: { source: skillId ? "cloud-skill-binding" : "cloud-default", modelId: "gpt-5.4-mini", modelLabel: "GPT-5.4 Mini", routingMode: "platform-managed", skillId, skillName: skillId, message: "mock" } }), sendPrompt: async (sessionId: string, prompt: string, skillId?: string) => ({ sessionId: sessionId || "project:xiaohongshu:default", reply: { id: "reply-1", role: "assistant", content: "Mock: " + prompt, createdAt: new Date().toISOString() }, executionPolicy: { source: skillId ? "cloud-skill-binding" : "cloud-default", modelId: "gpt-5.4-mini", modelLabel: "GPT-5.4 Mini", routingMode: "platform-managed", skillId, skillName: skillId, message: "mock" } }),
streamPrompt: async (_sessionId: string, prompt: string, skillId?: string) => { streamPrompt: async (_sessionId: string, prompt: string, skillId?: string) => {
const requestId = createClientMessageId("mock-request"); const requestId = createClientMessageId("mock-request");
...@@ -458,20 +526,111 @@ function getPluginCopy(plugin: WorkspaceSummary["plugins"][number]) { ...@@ -458,20 +526,111 @@ function getPluginCopy(plugin: WorkspaceSummary["plugins"][number]) {
return pluginDisplayMap[plugin.id] ?? { name: plugin.name, description: plugin.description }; return pluginDisplayMap[plugin.id] ?? { name: plugin.name, description: plugin.description };
} }
function getSkillStatusText(skill: WorkspaceSummary["skills"][number]) { type ExpertProject = WorkspaceSummary["projects"][number];
switch (skill.downloadState) {
case "ready": interface ExpertGuideContent {
return "Ready"; greeting: string;
case "downloading": summary: string;
return "Syncing"; prompts: string[];
case "failed": }
return skill.lastError ? `Failed: ${skill.lastError}` : "Failed";
case "pending": function getProjectDisplayName(project: ExpertProject | undefined): string {
return "Pending"; return project?.displayName ?? project?.name ?? ui.defaultChat;
case "removed": }
return "Removed";
function formatSessionTitle(title: string, index: number): string {
const normalized = title.trim();
if (!normalized) {
return `会话记录 ${index + 1}`;
}
if (/^new session\s*(\d+)$/i.test(normalized)) {
const matched = /^new session\s*(\d+)$/i.exec(normalized);
return `会话记录 ${matched?.[1] ?? String(index + 1)}`;
}
if (/^default session$/i.test(normalized) || normalized === ui.defaultChat) {
return "主会话";
}
return normalized;
}
function deriveSidebarSessionTitle(messages: ChatMessage[]): string {
const firstUserMessage = messages.find((message) => message.role === "user" && message.content.trim());
if (!firstUserMessage) {
return "待开始对话";
}
const normalized = firstUserMessage.content.replace(/\s+/g, " ").trim();
if (!normalized) {
return "待开始对话";
}
const preview = [...normalized].slice(0, 5).join("");
return normalized.length > preview.length ? `${preview}...` : preview;
}
function resolveExpertKey(project: ExpertProject | undefined): "xiaohongshu" | "douyin" | "browser" | "general" {
const seed = [project?.platform, project?.displayName, project?.name, project?.id]
.filter(Boolean)
.join(" ")
.toLowerCase();
if (/xiaohongshu|xhs|rednote|小红书/.test(seed)) {
return "xiaohongshu";
}
if (/douyin|tiktok|抖音/.test(seed)) {
return "douyin";
}
if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) {
return "browser";
}
return "general";
}
function getExpertGuide(project: ExpertProject | undefined): ExpertGuideContent {
switch (resolveExpertKey(project)) {
case "xiaohongshu":
return {
greeting: "从选题、文案到发布节奏,都可以直接交给我。",
summary: "聚焦小红书选题策划、笔记结构优化、素材整理和发布建议。",
prompts: [
"帮我规划 7 天的小红书选题日历,主题是职场效率。",
"把这条产品卖点整理成一篇小红书笔记结构。",
"给我 5 个适合小红书的爆款标题方向。",
"根据小红书风格,帮我润色这段口播文案。"
]
};
case "douyin":
return {
greeting: "你可以直接说目标,我会按抖音内容和发布节奏来拆解。",
summary: "适合抖音脚本策划、短视频结构、口播优化和发布执行建议。",
prompts: [
"帮我规划 5 条抖音短视频选题,目标是引流到私域。",
"把这个产品介绍改成 30 秒抖音口播脚本。",
"给我一个抖音视频的镜头脚本和字幕节奏。",
"分析这个选题为什么更适合抖音而不是小红书。"
]
};
case "browser":
return {
greeting: "适合处理浏览器自动化、采集、表单填写和发布流程设计。",
summary: "围绕浏览器操作自动化、信息采集、流程编排和执行前检查提供帮助。",
prompts: [
"帮我设计一个浏览器自动采集选题的执行流程。",
"梳理抖音发布前需要自动检查的页面步骤。",
"如果要自动登录并抓取页面数据,我应该先确认哪些风险点?",
"把小红书和抖音的发布流程拆成可自动化的步骤。"
]
};
default: default:
return "Unknown"; return {
greeting: "说清你的目标,我会先帮你拆成可执行步骤。",
summary: "适合梳理需求、生成方案、组织信息和推进执行。",
prompts: [
"先帮我梳理这个任务的目标、输入和输出。",
"把我的需求拆成 3 个最先执行的步骤。",
"根据当前专家能力,给我一个最实用的处理方案。"
]
};
} }
} }
...@@ -549,6 +708,7 @@ export default function App() { ...@@ -549,6 +708,7 @@ export default function App() {
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
const [infoText, setInfoText] = useState(""); const [infoText, setInfoText] = useState("");
const [messageTraces, setMessageTraces] = useState<Record<string, MessageTraceState>>({}); const [messageTraces, setMessageTraces] = useState<Record<string, MessageTraceState>>({});
const [sidebarSessionTitles, setSidebarSessionTitles] = useState<Record<string, string>>({});
const [skillMenuOpen, setSkillMenuOpen] = useState(false); const [skillMenuOpen, setSkillMenuOpen] = useState(false);
const activeStreamRef = useRef<ActiveStreamState | null>(null); const activeStreamRef = useRef<ActiveStreamState | null>(null);
const skillMenuRef = useRef<HTMLDivElement | null>(null); const skillMenuRef = useRef<HTMLDivElement | null>(null);
...@@ -571,6 +731,16 @@ export default function App() { ...@@ -571,6 +731,16 @@ export default function App() {
: startupCurtainCopy.loadingLabel; : startupCurtainCopy.loadingLabel;
const projects = workspace?.projects ?? []; const projects = workspace?.projects ?? [];
const sessions = !setupRequired ? (workspace?.sessions ?? [{ id: activeSessionId, projectId: workspace?.currentProjectId ?? "default", title: ui.defaultChat, updatedAt: new Date().toISOString() }]) : []; const sessions = !setupRequired ? (workspace?.sessions ?? [{ id: activeSessionId, projectId: workspace?.currentProjectId ?? "default", title: ui.defaultChat, updatedAt: new Date().toISOString() }]) : [];
const activeProject = useMemo(() => projects.find((project) => project.id === workspace?.currentProjectId) ?? projects[0], [projects, workspace?.currentProjectId]);
const activeExpertName = useMemo(() => getProjectDisplayName(activeProject), [activeProject]);
const activeExpertGuide = useMemo(() => getExpertGuide(activeProject), [activeProject]);
const expertPageProjects = useMemo(() => projects.slice(0, 2), [projects]);
const expertCards = useMemo(() => expertPageProjects.map((project) => ({
project,
displayName: getProjectDisplayName(project),
guide: getExpertGuide(project),
isActive: project.id === activeProject?.id
})), [activeProject?.id, expertPageProjects]);
const resolvedActiveSessionId = useMemo(() => resolvePreferredSessionId(sessions, activeSessionId), [activeSessionId, sessions]); const resolvedActiveSessionId = useMemo(() => resolvePreferredSessionId(sessions, activeSessionId), [activeSessionId, sessions]);
const isBound = !setupRequired; const isBound = !setupRequired;
const hasActiveProject = Boolean(workspace?.projectReady && workspace?.currentProjectId); const hasActiveProject = Boolean(workspace?.projectReady && workspace?.currentProjectId);
...@@ -588,8 +758,9 @@ export default function App() { ...@@ -588,8 +758,9 @@ export default function App() {
const setupActionDisabled = saving || !apiKeyDraft.trim() || (isDirectProviderSetup && (!baseUrlDraft.trim() || !defaultModelDraft.trim())); const setupActionDisabled = saving || !apiKeyDraft.trim() || (isDirectProviderSetup && (!baseUrlDraft.trim() || !defaultModelDraft.trim()));
const showBindEntry = !isBound && !showStartupOverlay; const showBindEntry = !isBound && !showStartupOverlay;
const showSettingsStatusHint = viewMode === "settings" && isBound && chatLaunchState !== "ready" && Boolean(startupMessage); const showSettingsStatusHint = viewMode === "settings" && isBound && chatLaunchState !== "ready" && Boolean(startupMessage);
const pageTitle = viewMode === "chat" ? ui.chat : viewMode === "skills" ? ui.skills : viewMode === "plugins" ? ui.plugins : ui.settings; const isConversationView = viewMode === "chat" || viewMode === "experts";
const pageDesc = viewMode === "chat" ? ui.chatPageDesc : viewMode === "skills" ? ui.skillsPageDesc : viewMode === "plugins" ? ui.pluginsPageDesc : ui.settingsDesc; const pageTitle = viewMode === "plugins" ? ui.plugins : ui.settings;
const pageDesc = viewMode === "plugins" ? ui.pluginsPageDesc : ui.settingsDesc;
useEffect(() => { useEffect(() => {
if (!infoText) { if (!infoText) {
return; return;
...@@ -751,6 +922,18 @@ export default function App() { ...@@ -751,6 +922,18 @@ export default function App() {
}; };
}, [skillMenuOpen]); }, [skillMenuOpen]);
useEffect(() => {
if (viewMode !== "experts" || !expertPageProjects.length || projectActionPending) {
return;
}
if (expertPageProjects.some((project) => project.id === activeProject?.id)) {
return;
}
void switchProject(expertPageProjects[0].id);
}, [activeProject?.id, expertPageProjects, projectActionPending, viewMode]);
useEffect(() => { useEffect(() => {
if (!config) { if (!config) {
return; return;
...@@ -770,6 +953,42 @@ export default function App() { ...@@ -770,6 +953,42 @@ export default function App() {
void loadMessages(resolvedActiveSessionId, true, false); void loadMessages(resolvedActiveSessionId, true, false);
}, [gatewayStatus, isBound, resolvedActiveSessionId, runtimeStatus, workspace?.chatReady]); }, [gatewayStatus, isBound, resolvedActiveSessionId, runtimeStatus, workspace?.chatReady]);
useEffect(() => {
let cancelled = false;
async function hydrateSidebarSessionTitles() {
if (!sessions.length) {
if (!cancelled) {
setSidebarSessionTitles({});
}
return;
}
const nextEntries = await Promise.all(sessions.map(async (session, index) => {
if (session.id === resolvedActiveSessionId && messages.length) {
return [session.id, deriveSidebarSessionTitle(toPlainMessages(messages))] as const;
}
try {
const rawMessages = await desktopApi.chat.listMessages(session.id);
const visibleMessages = rawMessages.filter(isPrimaryChatMessage);
return [session.id, deriveSidebarSessionTitle(visibleMessages)] as const;
} catch {
return [session.id, formatSessionTitle(session.title, index)] as const;
}
}));
if (!cancelled) {
setSidebarSessionTitles(Object.fromEntries(nextEntries));
}
}
void hydrateSidebarSessionTitles();
return () => {
cancelled = true;
};
}, [desktopApi.chat, messages, resolvedActiveSessionId, sessions]);
async function switchProject(projectId: string) { async function switchProject(projectId: string) {
if (projectActionPending) { if (projectActionPending) {
return; return;
...@@ -1488,16 +1707,46 @@ export default function App() { ...@@ -1488,16 +1707,46 @@ export default function App() {
await submitPrompt(prompt, skillId); await submitPrompt(prompt, skillId);
} }
async function handleComposerKeyDown(event: ReactKeyboardEvent<HTMLTextAreaElement>) {
if (event.key !== "Enter" || event.shiftKey || event.altKey) {
return;
}
if (!(event.ctrlKey || event.metaKey)) {
return;
}
event.preventDefault();
await sendPrompt();
}
function chooseSkill(skillId: string) { function chooseSkill(skillId: string) {
setSelectedSkillId(skillId); setSelectedSkillId(skillId);
setSkillMenuOpen(false); setSkillMenuOpen(false);
} }
function applyStarterPrompt(nextPrompt: string) {
setViewMode((current) => (current === "experts" ? "experts" : "chat"));
setPrompt(nextPrompt);
}
function clearSelectedSkill() { function clearSelectedSkill() {
setSelectedSkillId(DEFAULT_SKILL.id); setSelectedSkillId(DEFAULT_SKILL.id);
setSkillMenuOpen(false); setSkillMenuOpen(false);
} }
function openSession(sessionId: string) {
setViewMode((current) => (current === "experts" ? "experts" : "chat"));
setActiveSessionId(sessionId);
}
async function switchExpert(projectId: string) {
if (projectId === activeProject?.id) {
return;
}
await switchProject(projectId);
}
async function retryStartup() { async function retryStartup() {
setErrorText(""); setErrorText("");
await desktopApi.workspace.warmup().catch(() => undefined); await desktopApi.workspace.warmup().catch(() => undefined);
...@@ -1516,81 +1765,31 @@ export default function App() { ...@@ -1516,81 +1765,31 @@ export default function App() {
} }
} }
return ( const sidebarSessionLabel = "会话管理";
<div className="shell"> const selectedSkillBadge = selectedSkillId === DEFAULT_SKILL.id ? "自由对话" : "@" + selectedSkill.name;
<aside className="sidebar"> const panelNewSessionAction = (
<div className="brand-block">
<span className="brand-kicker">{ui.subtitle}</span>
<h1>{ui.app}</h1>
<p>{ui.appDesc}</p>
</div>
<nav className="nav-list">
{[{ id: "chat" as const, label: ui.chat }, { id: "skills" as const, label: ui.skills }, { id: "plugins" as const, label: ui.plugins }, { id: "settings" as const, label: ui.settings }].map((item) => (
<button key={item.id} type="button" className={"nav-item" + (viewMode === item.id ? " active" : "")} onClick={() => setViewMode(item.id)}>{item.label}</button>
))}
</nav>
</aside>
<div className="main-shell">
<div className="page-topbar">
{viewMode === "chat" ? (
<p className="hero-line">{ui.heroLine}</p>
) : (
<div className="page-copy">
<h2>{pageTitle}</h2>
<p>{pageDesc}</p>
</div>
)}
<div className="header-actions">
{viewMode === "chat" && isBound ? <StatusChip tone="positive">{ui.bound}</StatusChip> : null}
{isMockDesktopApi ? <StatusChip tone="warning">Mock API</StatusChip> : null}
</div>
</div>
{infoText ? <div className="notice toast-notice">{infoText}</div> : null}
{errorText ? <div className="notice error">{errorText}</div> : null}
<main className="content-area">
{viewMode === "chat" ? (
<section className="panel chat-panel">
<>
{!showBindEntry && projects.length > 0 ? (
<div className="project-switcher">
<div className="project-switcher-head">
<span>{ui.projectSwitcherLabel}</span>
<strong>{workspace?.currentProjectName ?? projects[0]?.name ?? ui.defaultChat}</strong>
</div>
<div className="project-chip-row">
{projects.map((project) => (
<button <button
key={project.id}
type="button" type="button"
className={"project-chip" + (workspace?.currentProjectId === project.id ? " active" : "")} className="secondary conversation-new-session"
disabled={projectActionPending} disabled={projectActionPending || !isBound || !projects.length}
onClick={() => void switchProject(project.id)} onClick={() => void createProjectSession()}
> >
{project.name} 新建对话
</button> </button>
))} );
</div> const conversationPanelTitle = viewMode === "experts" ? activeExpertName : homeChatCopy.title;
</div> const conversationPanelLead = viewMode === "chat" ? (
) : null} <div className="home-microcopy" aria-label="start your idea">
{!showBindEntry && sessions.length > 0 ? ( <span className="home-microcopy-icon">
<div className="session-strip"> <LobsterClawIcon />
<div className="session-strip-head"> </span>
<span>{ui.projectSessionsLabel}</span> <span className="home-microcopy-text">{homeChatCopy.microcopy}</span>
<button type="button" className="session-add" disabled={projectActionPending || !isBound || !projects.length} onClick={() => void createProjectSession()}>+ {ui.newSession}</button> <span className="home-microcopy-tag">{selectedSkillBadge}</span>
</div>
<div className="session-tab-row">
{sessions.map((session) => (
<div key={session.id} className={"session-tab" + (activeSessionId === session.id ? " active" : "")}>
<button type="button" className="session-tab-main" disabled={projectActionPending} onClick={() => setActiveSessionId(session.id)}>{session.title}</button>
{sessions.length > 1 ? (
<button type="button" className="session-tab-close" aria-label={ui.closeSession} disabled={projectActionPending} onClick={() => void closeProjectSession(session.id)}>?</button>
) : null}
</div>
))}
</div>
</div> </div>
) : null} ) : (
{showBindEntry ? ( <div className="conversation-panel-kicker">{conversationPanelTitle}</div>
);
const bindEntryContent = (
<div className="bind-entry"> <div className="bind-entry">
<div className="bind-entry-copy"> <div className="bind-entry-copy">
<strong>{ui.bindTitle}</strong> <strong>{ui.bindTitle}</strong>
...@@ -1601,7 +1800,32 @@ export default function App() { ...@@ -1601,7 +1800,32 @@ export default function App() {
<button disabled={saving || apiKeyDraft.trim().length === 0} onClick={() => void saveConfig(apiKeyDraft)}>{saving ? ui.binding : ui.bindNow}</button> <button disabled={saving || apiKeyDraft.trim().length === 0} onClick={() => void saveConfig(apiKeyDraft)}>{saving ? ui.binding : ui.bindNow}</button>
</div> </div>
</div> </div>
) : null} );
const activeEmptyState = viewMode === "experts" ? (
<div className="empty-state expert-empty-state">
<span className="empty-state-kicker">{activeExpertName}</span>
<strong>{activeExpertGuide.greeting}</strong>
<p>{ui.starterQuestionsHint}</p>
<div className="starter-prompt-list">
{activeExpertGuide.prompts.map((item) => (
<button key={item} type="button" className="starter-prompt" onClick={() => applyStarterPrompt(item)}>{item}</button>
))}
</div>
</div>
) : (
<div className="empty-state home-empty-state">
<span className="empty-state-kicker">{selectedSkillBadge}</span>
<strong>{homeChatCopy.emptyTitle}</strong>
<p>{homeChatCopy.emptyDesc}</p>
<div className="starter-prompt-list">
{homeChatCopy.prompts.map((item) => (
<button key={item} type="button" className="starter-prompt" onClick={() => applyStarterPrompt(item)}>{item}</button>
))}
</div>
</div>
);
const messageListContent = (
<div className="message-list"> <div className="message-list">
{messages.map((message) => { {messages.map((message) => {
const showThinking = message.role === "assistant" && message.streamState === "streaming" && !message.content.trim(); const showThinking = message.role === "assistant" && message.streamState === "streaming" && !message.content.trim();
...@@ -1610,7 +1834,7 @@ export default function App() { ...@@ -1610,7 +1834,7 @@ export default function App() {
const isTraceExpanded = Boolean(messageTrace?.expanded); const isTraceExpanded = Boolean(messageTrace?.expanded);
return ( return (
<article key={message.id} className={"message-card " + message.role + (message.streamState ? " " + message.streamState : "")}> <article key={message.id} className={"message-card " + message.role + (message.streamState ? " " + message.streamState : "")}>
<header><strong>{message.role === "assistant" ? ui.app : message.role === "user" ? ui.user : ui.system}</strong></header> <div className="message-bubble">
{showThinking ? ( {showThinking ? (
<div className="thinking-indicator" aria-live="polite"> <div className="thinking-indicator" aria-live="polite">
<span className="thinking-spinner" aria-hidden="true" /> <span className="thinking-spinner" aria-hidden="true" />
...@@ -1640,14 +1864,29 @@ export default function App() { ...@@ -1640,14 +1864,29 @@ export default function App() {
) : null} ) : null}
</div> </div>
) : null} ) : null}
</div>
</article> </article>
); );
})} })}
{!messages.length ? <div className="empty-state">{ui.noMessages}</div> : null} {!messages.length && !showBindEntry ? activeEmptyState : null}
</div> </div>
);
const conversationBodyContent = showBindEntry
? bindEntryContent
: viewMode === "experts" && !expertPageProjects.length
? <div className="empty-state">{expertsPageCopy.noExperts}</div>
: messageListContent;
const composerContent = (
<div className="composer-shell"> <div className="composer-shell">
<label className="composer-field"> <label className="composer-field">
<textarea value={prompt} disabled={!isBound} onChange={(event) => setPrompt(event.target.value)} placeholder={isBound ? ui.taskPlaceholder : ui.taskDisabledPlaceholder} /> <textarea
value={prompt}
disabled={!isBound}
onChange={(event) => setPrompt(event.target.value)}
onKeyDown={(event) => void handleComposerKeyDown(event)}
placeholder={isBound ? ui.taskPlaceholder : ui.taskDisabledPlaceholder}
/>
</label> </label>
<div className="composer-footer"> <div className="composer-footer">
<div className="composer-left-tools" ref={skillMenuRef}> <div className="composer-left-tools" ref={skillMenuRef}>
...@@ -1680,13 +1919,109 @@ export default function App() { ...@@ -1680,13 +1919,109 @@ export default function App() {
</div> </div>
) : null} ) : null}
</div> </div>
<button disabled={!canSend} onClick={() => void sendPrompt()}>{sendButtonLabel}</button> <button className="composer-submit" disabled={!canSend} onClick={() => void sendPrompt()}>{sendButtonLabel}</button>
</div> </div>
</div> </div>
</> );
return (
<div className="shell">
<aside className="sidebar">
<div className="sidebar-top">
<div className="brand-block">
<h1 className="brand-title">
<span className="brand-name brand-name-primary">千匠</span>
<span className="brand-divider" aria-hidden="true" />
<span className="brand-name brand-name-secondary">问天</span>
</h1>
</div>
<nav className="nav-list">
{[{ id: "chat" as const, label: homeChatCopy.title }, { id: "experts" as const, label: ui.experts }, { id: "plugins" as const, label: ui.plugins }, { id: "settings" as const, label: ui.settings }].map((item) => (
<button key={item.id} type="button" className={"nav-item" + (viewMode === item.id ? " active" : "")} onClick={() => setViewMode(item.id)}>{item.label}</button>
))}
</nav>
</div>
<div className="sidebar-bottom">
<section className="sidebar-section compact sidebar-experts-entry">
<div className="sidebar-section-head">
<div className="sidebar-section-copy">
<span className="sidebar-section-label">专家列表</span>
</div>
</div>
{expertPageProjects.length ? (
<div className="expert-chip-list preview">
{expertCards.map(({ project, displayName, isActive }) => (
<button
key={project.id}
type="button"
className={"expert-chip" + (isActive ? " active" : "")}
disabled={projectActionPending}
onClick={() => {
setViewMode("experts");
void switchExpert(project.id);
}}
>
<span className="expert-chip-copy">{displayName}</span>
</button>
))}
</div>
) : null}
</section>
{!showBindEntry && sessions.length > 0 ? (
<section className="sidebar-section sidebar-section-fill compact">
<div className="sidebar-section-head">
<div className="sidebar-section-copy">
<span className="sidebar-section-label">{sidebarSessionLabel}</span>
</div>
</div>
<div className="sidebar-session-list">
{sessions.map((session, index) => (
<div key={session.id} className={"sidebar-session-card" + (activeSessionId === session.id ? " active" : "")}>
<button type="button" className="sidebar-session-main" disabled={projectActionPending} onClick={() => openSession(session.id)}>
<strong>{sidebarSessionTitles[session.id] ?? formatSessionTitle(session.title, index)}</strong>
</button>
{sessions.length > 1 ? (
<button type="button" className="sidebar-session-close" aria-label={ui.closeSession} disabled={projectActionPending} onClick={() => void closeProjectSession(session.id)}>x</button>
) : null}
</div>
))}
</div>
</section>
) : null}
</div>
</aside>
<div className="main-shell">
{!isConversationView ? (
<div className="page-topbar">
<div className="page-copy">
<h2>{pageTitle}</h2>
<p>{pageDesc}</p>
</div>
<div className="header-actions">
{isMockDesktopApi ? <StatusChip tone="warning">Mock API</StatusChip> : null}
</div>
</div>
) : null}
{infoText ? <div className="notice toast-notice">{infoText}</div> : null}
{errorText ? <div className="notice error">{errorText}</div> : null}
<main className="content-area">
{isConversationView ? (
<section className="panel chat-panel conversation-panel">
<div className="conversation-panel-head">
<div className="conversation-panel-copy">
{conversationPanelLead}
</div>
<div className="conversation-panel-actions">
{isMockDesktopApi ? <StatusChip tone="warning">Mock API</StatusChip> : null}
{!showBindEntry ? panelNewSessionAction : null}
</div>
</div>
<div className="conversation-panel-body">
{conversationBodyContent}
</div>
{composerContent}
</section> </section>
) : null} ) : null}
{viewMode === "skills" ? <section className="panel catalog-list"><div className="scroll-panel">{catalogSkills.map((skill) => <button key={skill.id} type="button" className="catalog-item" disabled={!skill.ready} onClick={() => { if (!skill.ready) { return; } setSelectedSkillId(skill.id); setViewMode("chat"); }}><strong>{skill.name}</strong><p>{skill.description}</p><p>{getSkillStatusText(skill)}{skill.fileName ? ` - ${skill.fileName}` : ""}</p></button>)}{!catalogSkills.length ? <div className="empty-state">{ui.noSkillCards}</div> : null}</div></section> : null}
{viewMode === "plugins" ? <section className="panel catalog-list"><div className="section-head compact"><div><h3>{ui.pluginTitle}</h3></div></div><div className="scroll-panel">{workspace?.plugins.map((plugin) => { const copy = getPluginCopy(plugin); return <article key={plugin.id} className="catalog-item static"><strong>{copy.name}</strong><p>{copy.description}</p></article>; })}{!workspace?.plugins.length ? <div className="empty-state">{ui.noPlugins}</div> : null}</div></section> : null} {viewMode === "plugins" ? <section className="panel catalog-list"><div className="section-head compact"><div><h3>{ui.pluginTitle}</h3></div></div><div className="scroll-panel">{workspace?.plugins.map((plugin) => { const copy = getPluginCopy(plugin); return <article key={plugin.id} className="catalog-item static"><strong>{copy.name}</strong><p>{copy.description}</p></article>; })}{!workspace?.plugins.length ? <div className="empty-state">{ui.noPlugins}</div> : null}</div></section> : null}
{viewMode === "settings" ? ( {viewMode === "settings" ? (
<div className="page-stack"> <div className="page-stack">
...@@ -1825,44 +2160,3 @@ export default function App() { ...@@ -1825,44 +2160,3 @@ export default function App() {
</div> </div>
); );
} }
...@@ -147,6 +147,219 @@ strong { font-weight: 600; } ...@@ -147,6 +147,219 @@ strong { font-weight: 600; }
box-shadow: inset 3px 0 0 #0f7bff, 0 8px 18px rgba(15, 123, 255, 0.1); box-shadow: inset 3px 0 0 #0f7bff, 0 8px 18px rgba(15, 123, 255, 0.1);
} }
.sidebar-top,
.sidebar-bottom,
.sidebar-section,
.expert-card-list,
.sidebar-session-list,
.chat-header-copy,
.chat-header-meta,
.chat-header-stat,
.expert-empty-state,
.starter-prompt-list {
display: grid;
gap: 10px;
}
.sidebar-top,
.sidebar-bottom {
min-height: 0;
}
.sidebar-bottom {
overflow: auto;
padding-right: 2px;
align-content: start;
}
.sidebar-section {
padding: 12px;
border-radius: 18px;
border: 1px solid rgba(209, 220, 236, 0.92);
background: rgba(255, 255, 255, 0.78);
box-shadow: 0 12px 28px rgba(35, 52, 82, 0.06);
}
.sidebar-section-fill {
min-height: 0;
}
.sidebar-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.sidebar-section-head span,
.sidebar-section-head strong,
.expert-card-kicker,
.expert-card-meta,
.sidebar-session-main span,
.chat-header-kicker,
.chat-header-stat span,
.empty-state-kicker,
.sidebar-inline-action {
font-size: 12px;
line-height: 1.5;
}
.sidebar-section-head span,
.expert-card-kicker,
.expert-card-meta,
.sidebar-session-main span,
.chat-header-kicker,
.chat-header-stat span,
.empty-state-kicker {
color: #6a7b96;
}
.sidebar-inline-action {
padding: 0;
background: transparent;
color: #0f67de;
}
.expert-card {
width: 100%;
display: grid;
gap: 6px;
padding: 12px;
text-align: left;
border-radius: 16px;
border: 1px solid rgba(209, 220, 236, 0.9);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(245, 249, 255, 0.94));
color: #20304b;
transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
}
.expert-card:hover {
transform: translateY(-1px);
box-shadow: 0 14px 30px rgba(35, 52, 82, 0.08);
}
.expert-card.active {
border-color: rgba(15, 123, 255, 0.3);
box-shadow: 0 16px 34px rgba(15, 123, 255, 0.12);
}
.expert-card strong,
.sidebar-session-main strong,
.chat-header-copy h2,
.chat-header-stat strong,
.expert-empty-state strong {
color: #1f2f49;
}
.expert-card p,
.chat-header-copy p,
.expert-empty-state p {
color: #667794;
font-size: 13px;
line-height: 1.7;
}
.sidebar-session-list {
min-height: 0;
overflow: auto;
}
.sidebar-session-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: stretch;
padding: 8px;
border-radius: 14px;
border: 1px solid transparent;
background: rgba(247, 250, 255, 0.88);
}
.sidebar-session-card.active {
border-color: rgba(15, 123, 255, 0.18);
background: rgba(238, 245, 255, 0.96);
}
.sidebar-session-main,
.sidebar-session-close {
border: none;
background: transparent;
color: #20304b;
}
.sidebar-session-main {
min-width: 0;
display: grid;
gap: 2px;
padding: 0;
text-align: left;
}
.sidebar-session-close {
width: 28px;
height: 28px;
padding: 0;
border-radius: 999px;
color: #7c8da8;
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 12px 14px;
border-radius: 18px;
border: 1px solid rgba(209, 220, 236, 0.92);
background: linear-gradient(180deg, rgba(250, 252, 255, 0.98), rgba(244, 248, 255, 0.94));
}
.chat-header-copy {
min-width: 0;
}
.chat-header-copy h2 {
font-size: 22px;
line-height: 1.2;
}
.chat-header-meta {
grid-template-columns: repeat(2, minmax(92px, 1fr));
}
.chat-header-stat {
gap: 2px;
padding: 10px 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(215, 226, 241, 0.9);
text-align: center;
}
.chat-header-stat strong {
font-size: 18px;
}
.expert-empty-state {
gap: 12px;
background: linear-gradient(180deg, #f8fbff, #f2f7ff);
}
.starter-prompt-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.starter-prompt {
width: 100%;
min-height: 52px;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(206, 218, 235, 0.9);
background: rgba(255, 255, 255, 0.96);
color: #22324c;
text-align: left;
line-height: 1.6;
}
.main-shell { .main-shell {
height: 100vh; height: 100vh;
min-height: 0; min-height: 0;
...@@ -479,8 +692,8 @@ strong { font-weight: 600; } ...@@ -479,8 +692,8 @@ strong { font-weight: 600; }
} }
.message-card { padding: 16px; } .message-card { padding: 16px; }
.message-card.user { background: #eef5ff; } .message-card.user { background: transparent; }
.message-card.assistant { background: #eefbf7; } .message-card.assistant { background: transparent; }
.message-card.streaming { border-color: #b7e4d5; } .message-card.streaming { border-color: #b7e4d5; }
.message-card.error { border-color: rgba(239, 68, 68, 0.24); } .message-card.error { border-color: rgba(239, 68, 68, 0.24); }
.message-card p { .message-card p {
...@@ -713,8 +926,8 @@ strong { font-weight: 600; } ...@@ -713,8 +926,8 @@ strong { font-weight: 600; }
} }
.composer-shell { .composer-shell {
gap: 12px; gap: 10px;
padding: 14px; padding: 12px;
border-radius: 16px; border-radius: 16px;
border: 1px solid #dbe5f1; border: 1px solid #dbe5f1;
background: linear-gradient(180deg, rgba(248, 251, 255, 0.98), rgba(255, 255, 255, 0.98)); background: linear-gradient(180deg, rgba(248, 251, 255, 0.98), rgba(255, 255, 255, 0.98));
...@@ -726,7 +939,7 @@ strong { font-weight: 600; } ...@@ -726,7 +939,7 @@ strong { font-weight: 600; }
} }
.composer-field textarea { .composer-field textarea {
min-height: 124px; min-height: 88px;
} }
.composer-footer { .composer-footer {
...@@ -893,6 +1106,10 @@ strong { font-weight: 600; } ...@@ -893,6 +1106,10 @@ strong { font-weight: 600; }
border-right: 0; border-right: 0;
border-bottom: 1px solid #dee6f1; border-bottom: 1px solid #dee6f1;
} }
.sidebar-bottom {
grid-template-columns: repeat(2, minmax(0, 1fr));
overflow: visible;
}
.main-shell { .main-shell {
height: 100%; height: 100%;
} }
...@@ -924,7 +1141,9 @@ strong { font-weight: 600; } ...@@ -924,7 +1141,9 @@ strong { font-weight: 600; }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.startup-overlay-panel, .startup-overlay-panel,
.startup-overlay-progress span, .startup-overlay-progress span,
.toast-notice { .toast-notice,
.experts-drawer,
.button-chevron {
animation: none; animation: none;
transition: none; transition: none;
} }
...@@ -1021,3 +1240,1274 @@ strong { font-weight: 600; } ...@@ -1021,3 +1240,1274 @@ strong { font-weight: 600; }
padding: 0; padding: 0;
color: #0a4f9d; color: #0a4f9d;
} }
@media (max-width: 1100px) {
.chat-header {
align-items: stretch;
flex-direction: column;
}
.chat-header-meta,
.starter-prompt-list {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.sidebar-section-head {
align-items: stretch;
flex-direction: column;
}
.sidebar-bottom,
.starter-prompt-list {
grid-template-columns: 1fr;
}
}
.sidebar {
grid-template-rows: auto minmax(0, 1fr);
}
.sidebar-bottom {
display: grid;
gap: 10px;
}
.sidebar-section.compact {
padding: 10px 12px;
gap: 8px;
border-radius: 16px;
}
.sidebar-section-head.stacked {
align-items: flex-start;
}
.sidebar-section-copy {
min-width: 0;
display: grid;
gap: 2px;
}
.sidebar-section-label {
color: #6a7b96;
font-size: 12px;
line-height: 1.5;
}
.sidebar-section-title {
color: #1f2f49;
font-size: 14px;
line-height: 1.45;
}
.expert-chip-list {
display: grid;
gap: 8px;
}
.expert-chip {
width: 100%;
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 9px 10px;
border-radius: 12px;
border: 1px solid rgba(209, 220, 236, 0.9);
background: rgba(247, 250, 255, 0.9);
color: #20304b;
text-align: left;
}
.expert-chip.active {
border-color: rgba(15, 123, 255, 0.24);
background: rgba(238, 245, 255, 0.96);
box-shadow: inset 0 0 0 1px rgba(15, 123, 255, 0.08);
}
.expert-chip-copy {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #1f2f49;
font-size: 13px;
font-weight: 600;
line-height: 1.4;
}
.expert-chip-count {
flex: 0 0 auto;
min-width: 24px;
height: 24px;
padding: 0 7px;
border-radius: 999px;
background: rgba(15, 123, 255, 0.1);
color: #0f67de;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
}
.sidebar-session-list {
gap: 8px;
}
.sidebar-session-card {
padding: 6px 8px;
border-radius: 12px;
}
.sidebar-session-main strong {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
line-height: 1.45;
}
.sidebar-session-close {
width: 24px;
height: 24px;
}
.page-topbar.compact {
align-items: center;
min-height: 28px;
}
.page-copy.compact {
gap: 0;
}
.page-copy.compact h2 {
font-size: 16px;
line-height: 1.3;
}
.chat-panel {
gap: 10px;
}
.chat-header.compact {
padding: 10px 12px;
gap: 12px;
align-items: flex-start;
}
.chat-header-main {
min-width: 0;
display: grid;
gap: 4px;
}
.chat-header-summary {
color: #667794;
font-size: 12px;
line-height: 1.6;
}
.sidebar-experts-entry {
background:
radial-gradient(circle at top right, rgba(15, 123, 255, 0.1), transparent 42%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(245, 249, 255, 0.94));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.82),
0 14px 30px rgba(35, 52, 82, 0.08);
}
.sidebar-entry-copy {
margin: 0;
color: #667794;
font-size: 12px;
line-height: 1.6;
}
.expert-chip-list.preview {
grid-template-columns: 1fr;
}
.page-copy.compact {
gap: 4px;
}
.page-copy.compact p {
max-width: 720px;
}
.page-topbar.compact {
align-items: flex-start;
min-height: auto;
}
.home-jump-card,
.experts-overview-bar {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(209, 220, 236, 0.92);
background: linear-gradient(135deg, rgba(247, 251, 255, 0.98), rgba(255, 255, 255, 0.96));
overflow: hidden;
}
.home-jump-card::before,
.experts-overview-bar::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 3px;
border-radius: inherit;
background: linear-gradient(180deg, #0f7bff 0%, #8fbfff 100%);
}
.home-jump-copy,
.experts-overview-copy {
min-width: 0;
display: grid;
gap: 6px;
}
.home-jump-copy span,
.experts-overview-copy span,
.experts-drawer-hint {
color: #6a7b96;
font-size: 12px;
line-height: 1.5;
}
.home-jump-copy strong,
.experts-overview-copy strong,
.experts-drawer-item strong {
color: #1f2f49;
}
.home-jump-copy strong {
font-size: 18px;
line-height: 1.35;
letter-spacing: -0.01em;
}
.experts-overview-copy strong {
font-size: 15px;
line-height: 1.45;
}
.home-jump-copy p,
.experts-drawer-item span {
margin: 0;
color: #667794;
font-size: 13px;
line-height: 1.65;
}
.home-jump-actions {
display: inline-flex;
align-items: center;
gap: 10px;
flex: 0 0 auto;
position: relative;
z-index: 1;
}
.home-jump-badge {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0 12px;
border-radius: 999px;
background: linear-gradient(135deg, rgba(255, 248, 223, 0.96), rgba(255, 242, 196, 0.88));
color: #8d6700;
font-size: 12px;
font-weight: 600;
border: 1px solid rgba(218, 180, 72, 0.28);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
}
.home-jump-button,
.experts-switcher-trigger {
border-radius: 999px;
}
.home-jump-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 112px;
}
.button-chevron {
width: 9px;
height: 9px;
display: inline-block;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(-45deg);
transition: transform 180ms ease, opacity 180ms ease;
opacity: 0.78;
}
.home-chat-header {
background: linear-gradient(180deg, rgba(252, 253, 255, 0.98), rgba(246, 249, 255, 0.94));
}
.home-empty-state {
gap: 12px;
background:
radial-gradient(circle at top right, rgba(15, 123, 255, 0.08), transparent 32%),
linear-gradient(180deg, #f8fbff, #f4f8ff);
}
.experts-overview-bar {
overflow: visible;
}
.experts-switcher {
position: relative;
flex: 0 0 auto;
}
.experts-switcher-trigger {
display: inline-flex;
align-items: center;
gap: 10px;
min-width: 168px;
justify-content: space-between;
text-align: left;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 248, 255, 0.96));
border: 1px solid rgba(209, 220, 236, 0.92);
box-shadow: 0 12px 24px rgba(35, 52, 82, 0.08);
}
.experts-switcher-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.experts-chevron {
width: 8px;
height: 8px;
transform: rotate(45deg);
opacity: 0.7;
}
.experts-chevron.open {
transform: rotate(225deg);
}
.experts-drawer {
position: absolute;
top: calc(100% + 10px);
right: 0;
z-index: 16;
width: min(360px, calc(100vw - 96px));
display: grid;
gap: 10px;
padding: 14px;
border-radius: 22px;
border: 1px solid rgba(219, 229, 241, 0.92);
background:
radial-gradient(circle at top right, rgba(15, 123, 255, 0.08), transparent 34%),
rgba(255, 255, 255, 0.96);
box-shadow: 0 24px 56px rgba(35, 52, 82, 0.18);
backdrop-filter: blur(14px);
transform-origin: top right;
animation: experts-drawer-enter 180ms ease-out both;
}
.experts-drawer-item {
width: 100%;
display: grid;
gap: 6px;
padding: 13px 14px;
text-align: left;
border-radius: 16px;
border: 1px solid rgba(209, 220, 236, 0.88);
background: rgba(247, 250, 255, 0.9);
color: #20304b;
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease, background 180ms ease;
}
.experts-drawer-item:hover {
transform: translateY(-1px);
border-color: rgba(15, 123, 255, 0.18);
box-shadow: 0 14px 28px rgba(35, 52, 82, 0.08);
}
.experts-drawer-item.active {
border-color: rgba(15, 123, 255, 0.26);
background: rgba(238, 245, 255, 0.96);
box-shadow: inset 0 0 0 1px rgba(15, 123, 255, 0.08);
}
.experts-drawer-item span {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
@keyframes experts-drawer-enter {
0% {
opacity: 0;
transform: translateY(-6px) scale(0.985);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.chat-header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.chat-header-tag {
padding: 7px 10px;
border-radius: 999px;
}
.expert-empty-state {
gap: 10px;
padding: 14px;
}
.starter-prompt-list {
grid-template-columns: 1fr;
gap: 8px;
}
.starter-prompt {
min-height: 44px;
padding: 10px 12px;
}
.message-list {
gap: 10px;
}
.message-card {
padding: 14px;
}
.composer-shell {
gap: 8px;
padding: 10px;
border-radius: 14px;
}
.composer-field textarea {
min-height: 68px;
max-height: 144px;
padding: 10px 12px;
}
.composer-footer {
align-items: center;
gap: 10px;
}
.skill-trigger {
width: 34px;
height: 34px;
}
.skill-chip {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.composer-submit {
min-width: 96px;
padding: 10px 16px;
}
@media (max-width: 1100px) {
.chat-header.compact {
flex-direction: column;
align-items: stretch;
}
.home-jump-card,
.experts-overview-bar {
align-items: stretch;
flex-direction: column;
}
.home-jump-actions {
justify-content: space-between;
}
.chat-header-actions {
justify-content: flex-start;
}
}
@media (max-width: 960px) {
.sidebar-bottom {
grid-template-columns: 1fr;
}
.expert-chip-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.expert-chip-list.preview {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.expert-chip-list {
grid-template-columns: 1fr;
}
.expert-chip-list.preview {
grid-template-columns: 1fr;
}
.chat-header-summary {
display: none;
}
.home-jump-actions {
width: 100%;
align-items: stretch;
flex-direction: column;
}
.home-jump-badge {
width: fit-content;
}
.experts-drawer {
left: 0;
right: auto;
width: min(100%, 360px);
}
.composer-submit {
width: 100%;
}
}
@media (max-height: 860px) {
.shell {
grid-template-columns: 192px minmax(0, 1fr);
}
.sidebar,
.main-shell {
padding: 14px;
gap: 10px;
}
.brand-block h1 {
font-size: 24px;
}
.brand-block p {
display: none;
}
.nav-item {
height: 38px;
}
.panel {
padding: 14px;
}
.chat-header.compact {
padding: 8px 10px;
}
.chat-header-copy h2 {
font-size: 20px;
}
.expert-empty-state {
padding: 12px;
}
.message-card {
padding: 12px;
}
.composer-shell {
padding: 8px;
}
.composer-field textarea {
min-height: 56px;
max-height: 120px;
}
}
:root {
color: #19304a;
background:
radial-gradient(circle at 0% 0%, rgba(147, 202, 255, 0.24), transparent 30%),
radial-gradient(circle at 100% 100%, rgba(148, 230, 213, 0.18), transparent 26%),
linear-gradient(180deg, #f7fbff 0%, #eff5fb 100%);
}
body {
background:
radial-gradient(circle at top left, rgba(133, 192, 255, 0.18), transparent 22%),
radial-gradient(circle at bottom right, rgba(155, 226, 214, 0.14), transparent 20%),
linear-gradient(180deg, #f7fbff 0%, #eef4fa 100%);
}
button {
border-radius: 14px;
background: linear-gradient(135deg, #1c7cf2, #1967dc);
box-shadow: 0 14px 28px rgba(39, 95, 166, 0.16);
transition: background 180ms ease, box-shadow 180ms ease, border-color 180ms ease, color 180ms ease;
}
button.secondary {
background: rgba(255, 255, 255, 0.9);
color: #28415d;
box-shadow: inset 0 0 0 1px rgba(199, 213, 230, 0.95);
}
.shell {
grid-template-columns: 220px minmax(0, 1fr);
}
.sidebar {
gap: 18px;
background:
linear-gradient(180deg, rgba(250, 252, 255, 0.96), rgba(240, 246, 252, 0.96)),
rgba(255, 255, 255, 0.82);
box-shadow: inset -1px 0 0 rgba(213, 224, 237, 0.88);
}
.brand-block {
gap: 12px;
padding: 4px 2px 6px;
}
.brand-mark {
width: 38px;
height: 10px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(82, 162, 250, 0.9), rgba(109, 208, 191, 0.72));
box-shadow: 0 10px 22px rgba(86, 155, 224, 0.2);
}
.brand-mark {
display: none;
}
.brand-title {
display: inline-flex;
align-items: center;
gap: 10px;
margin: 0;
font-size: 27px;
line-height: 1;
letter-spacing: 0.02em;
}
.brand-name {
display: inline-flex;
align-items: center;
font-weight: 700;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.brand-name-primary {
background-image: linear-gradient(135deg, #2c6fe8 0%, #5ba4ff 100%);
}
.brand-name-secondary {
background-image: linear-gradient(135deg, #3ea4bf 0%, #73cfc4 100%);
}
.brand-divider {
width: 10px;
height: 10px;
border-radius: 3px;
transform: rotate(45deg);
background: linear-gradient(135deg, rgba(77, 151, 232, 0.92), rgba(111, 204, 197, 0.78));
box-shadow: 0 8px 18px rgba(71, 138, 213, 0.16);
}
.nav-list {
gap: 10px;
}
.nav-item {
height: 44px;
border-radius: 14px;
color: #31455f;
}
.nav-item.active {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(242, 247, 255, 0.92));
color: #165fc7;
box-shadow: inset 3px 0 0 #2b7cf0, 0 12px 28px rgba(68, 119, 181, 0.12);
}
.sidebar-bottom {
gap: 12px;
}
.sidebar-section {
border-radius: 22px;
border: 1px solid rgba(215, 224, 236, 0.9);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 251, 255, 0.92));
box-shadow: 0 18px 36px rgba(36, 65, 101, 0.07);
}
.sidebar-section.compact {
padding: 12px;
gap: 10px;
}
.sidebar-section-head {
align-items: flex-start;
}
.sidebar-section-title {
font-size: 15px;
}
.sidebar-experts-entry {
background:
radial-gradient(circle at top right, rgba(90, 157, 255, 0.12), transparent 40%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(245, 249, 255, 0.95));
}
.expert-chip-list {
gap: 10px;
}
.expert-chip {
justify-content: flex-start;
padding: 12px 14px;
border-radius: 16px;
background: linear-gradient(180deg, rgba(251, 253, 255, 0.98), rgba(243, 248, 255, 0.94));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88);
}
.expert-chip.active {
border-color: rgba(57, 125, 226, 0.22);
background: linear-gradient(180deg, rgba(240, 247, 255, 0.98), rgba(233, 243, 255, 0.96));
box-shadow: inset 0 0 0 1px rgba(77, 141, 236, 0.08), 0 14px 28px rgba(66, 116, 176, 0.1);
}
.expert-chip-copy {
font-size: 13px;
line-height: 1.45;
}
.sidebar-session-list {
gap: 10px;
}
.sidebar-session-card {
padding: 8px 10px;
border-radius: 16px;
background: rgba(247, 250, 255, 0.92);
}
.sidebar-session-card.active {
border-color: rgba(60, 124, 224, 0.2);
background: linear-gradient(180deg, rgba(240, 246, 255, 0.98), rgba(236, 243, 254, 0.96));
box-shadow: 0 12px 26px rgba(68, 119, 181, 0.08);
}
.sidebar-session-close {
background: rgba(255, 255, 255, 0.76);
box-shadow: inset 0 0 0 1px rgba(217, 225, 236, 0.92);
}
.main-shell {
padding: 24px 24px 22px;
gap: 16px;
}
.page-topbar {
align-items: center;
}
.conversation-topbar {
min-height: 40px;
justify-content: flex-end;
}
.conversation-topbar .header-actions {
width: 100%;
justify-content: flex-end;
}
.conversation-new-session {
min-width: 112px;
padding: 9px 14px;
border-radius: 999px;
}
.panel,
.notice,
.empty-state,
.message-bubble,
.catalog-item {
border-radius: 24px;
}
.panel {
border-color: rgba(218, 226, 237, 0.9);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(250, 252, 255, 0.92));
box-shadow: 0 24px 48px rgba(38, 67, 102, 0.08);
}
.chat-panel {
gap: 14px;
grid-template-rows: auto minmax(0, 1fr) auto;
padding: 20px;
}
.home-microcopy {
display: inline-flex;
align-items: center;
gap: 10px;
align-self: flex-start;
min-height: 38px;
padding: 0 16px 0 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
box-shadow: inset 0 0 0 1px rgba(214, 224, 236, 0.94);
}
.home-microcopy-icon {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: linear-gradient(135deg, rgba(255, 240, 233, 0.98), rgba(255, 248, 242, 0.94));
box-shadow: 0 8px 18px rgba(220, 115, 103, 0.12);
}
.home-microcopy-icon svg {
width: 20px;
height: 20px;
}
.home-microcopy-text {
color: #4f6782;
font-size: 13px;
line-height: 1;
letter-spacing: 0.02em;
}
.home-microcopy-tag {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0 9px;
border-radius: 999px;
background: rgba(241, 246, 254, 0.96);
color: #5a7290;
font-size: 11px;
font-weight: 600;
box-shadow: inset 0 0 0 1px rgba(220, 228, 239, 0.92);
}
.empty-state {
border-style: solid;
border-color: rgba(219, 227, 238, 0.92);
background:
radial-gradient(circle at top right, rgba(107, 176, 255, 0.08), transparent 34%),
linear-gradient(180deg, rgba(250, 252, 255, 0.98), rgba(245, 249, 255, 0.96));
}
.message-list {
gap: 12px;
padding: 6px 2px 4px;
}
.message-card {
display: flex;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
}
.message-card.user {
justify-content: flex-end;
}
.message-card.assistant {
justify-content: flex-start;
}
.message-bubble {
width: fit-content;
max-width: min(78%, 760px);
display: grid;
gap: 8px;
padding: 14px 16px;
border: 1px solid rgba(218, 226, 237, 0.9);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(249, 252, 255, 0.96));
box-shadow: 0 16px 36px rgba(39, 65, 99, 0.08);
}
.message-card.user .message-bubble {
border-color: rgba(180, 211, 249, 0.9);
background: linear-gradient(180deg, rgba(233, 244, 255, 0.98), rgba(241, 248, 255, 0.96));
}
.message-card.assistant .message-bubble {
border-color: rgba(212, 227, 221, 0.94);
background: linear-gradient(180deg, rgba(244, 251, 248, 0.98), rgba(250, 253, 251, 0.96));
}
.message-card.streaming .message-bubble {
border-color: rgba(131, 201, 181, 0.72);
}
.message-card.error .message-bubble {
border-color: rgba(239, 68, 68, 0.24);
}
.message-card p {
margin: 0;
white-space: pre-wrap;
line-height: 1.76;
color: #20344d;
}
.thinking-indicator {
padding-top: 0;
}
.message-trace {
margin-top: 2px;
}
.trace-inline-toggle {
color: #53759e;
}
.composer-shell {
gap: 10px;
padding: 14px;
border-radius: 26px;
border-color: rgba(217, 225, 236, 0.96);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 251, 255, 0.96));
box-shadow: 0 18px 38px rgba(38, 67, 102, 0.08);
}
.composer-field textarea {
min-height: 92px;
max-height: 180px;
padding: 14px 16px;
border-radius: 22px;
border-color: rgba(214, 223, 235, 0.96);
background: linear-gradient(180deg, rgba(252, 253, 255, 0.98), rgba(248, 251, 255, 0.98));
line-height: 1.7;
}
.composer-footer {
align-items: center;
}
.skill-trigger,
.skill-chip {
border-radius: 999px;
}
.skill-trigger {
background: rgba(241, 246, 254, 0.98);
color: #4f709b;
box-shadow: inset 0 0 0 1px rgba(216, 225, 237, 0.96);
}
.skill-chip {
background: rgba(240, 246, 255, 0.94);
color: #45678f;
}
.composer-submit {
min-width: 102px;
border-radius: 999px;
}
@media (max-width: 1100px) {
.shell {
grid-template-columns: 1fr;
}
.conversation-topbar .header-actions {
justify-content: flex-start;
}
.message-bubble {
max-width: min(88%, 720px);
}
}
@media (max-width: 720px) {
.brand-title {
font-size: 24px;
}
.main-shell,
.sidebar {
padding: 16px;
}
.chat-panel {
padding: 16px;
}
.home-microcopy {
flex-wrap: wrap;
border-radius: 22px;
line-height: 1.5;
}
.message-bubble {
max-width: 100%;
}
.composer-shell {
padding: 12px;
border-radius: 22px;
}
.composer-field textarea {
min-height: 84px;
}
}
.main-shell {
padding-top: 16px;
gap: 10px;
}
.content-area {
display: grid;
}
.conversation-panel {
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 12px;
padding: 16px 18px 18px;
background: #ffffff;
border-color: #dfe6ef;
box-shadow: 0 18px 38px rgba(34, 58, 87, 0.06);
}
.conversation-panel-head {
min-height: 40px;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 0;
}
.conversation-panel-copy,
.conversation-panel-actions,
.conversation-panel-body {
min-width: 0;
}
.conversation-panel-copy {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1 1 auto;
}
.conversation-panel-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex: 0 0 auto;
}
.conversation-panel-kicker {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0 12px;
border-radius: 999px;
border: 1px solid #dfe6ef;
background: #fff;
color: #425a77;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
}
.conversation-panel-body {
min-height: 0;
overflow: hidden;
display: grid;
background: #ffffff;
border-radius: 18px;
}
.conversation-new-session {
min-width: 100px;
min-height: 32px;
padding: 0 14px;
border-radius: 999px;
}
.panel,
.catalog-item {
background: #fff;
}
.home-microcopy {
min-height: 32px;
padding: 0;
background: transparent;
box-shadow: none;
}
.home-microcopy-icon {
width: 24px;
height: 24px;
box-shadow: none;
}
.home-microcopy-text {
color: #4c6480;
}
.home-microcopy-tag {
background: #f6f8fb;
color: #5f738e;
box-shadow: inset 0 0 0 1px #e3e9f0;
}
.message-list {
height: 100%;
min-height: 0;
padding: 0;
background: #ffffff;
}
.empty-state {
background: #ffffff;
}
.composer-shell {
margin-top: 0;
padding: 12px;
border-radius: 20px;
background: #fff;
box-shadow: 0 10px 24px rgba(34, 58, 87, 0.05);
}
.composer-field textarea {
min-height: 88px;
max-height: 180px;
background: #fbfcfe;
}
.sidebar-section-title {
font-size: 14px;
font-weight: 600;
}
.sidebar-section-label {
color: #77879c;
letter-spacing: 0.04em;
}
.sidebar-session-main strong {
font-size: 12px;
color: #24374f;
}
.sidebar-session-card {
min-height: 40px;
}
.sidebar-section-copy {
gap: 0;
}
.sidebar-section-head {
min-height: 18px;
}
.sidebar-experts-entry,
.sidebar-section.sidebar-section-fill {
background: #ffffff;
}
.message-card,
.message-card.user,
.message-card.assistant {
justify-content: flex-start;
}
.message-bubble {
border-color: #e1e7ee;
background: #ffffff;
box-shadow: 0 10px 24px rgba(34, 58, 87, 0.05);
}
.message-card.user .message-bubble {
border-color: #d5e2f5;
background: #edf4ff;
}
.message-card.assistant .message-bubble {
border-color: #d7eadf;
background: #eefaf2;
}
@media (max-width: 720px) {
.conversation-panel {
padding: 14px;
}
.conversation-panel-head {
align-items: stretch;
flex-direction: column;
}
.conversation-panel-actions {
justify-content: flex-start;
}
.conversation-new-session {
width: 100%;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment