Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
5e8d2c51
Commit
5e8d2c51
authored
Apr 02, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refine desktop chat interface layout
parent
e74cd2e9
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
2015 additions
and
231 deletions
+2015
-231
App.tsx
apps/ui/src/App.tsx
+519
-225
styles.css
apps/ui/src/styles.css
+1496
-6
No files found.
apps/ui/src/App.tsx
View file @
5e8d2c51
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
;
import
type
{
KeyboardEvent
as
ReactKeyboardEvent
}
from
"react"
;
import
type
{
AppConfig
,
ChatLaunchState
,
...
...
@@ -20,7 +21,7 @@ import type {
WorkspaceSummary
}
from
"@qjclaw/shared-types"
;
type
ViewMode
=
"chat"
|
"
skill
s"
|
"plugins"
|
"settings"
;
type
ViewMode
=
"chat"
|
"
expert
s"
|
"plugins"
|
"settings"
;
type
Tone
=
"positive"
|
"warning"
;
type
MessageStreamState
=
"streaming"
|
"error"
;
type
SendPhase
=
"idle"
|
"preparing"
|
"streaming"
|
"finalizing"
;
...
...
@@ -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
=
{
id
:
"default-chat"
,
name
:
"
\
u9ed8
\
u8ba4
\
u5bf9
\
u8bdd"
,
...
...
@@ -166,7 +197,8 @@ const ui = {
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."
,
chat
:
"
\
u5bf9
\
u8bdd"
,
skills
:
"
\
u6280
\
u80fd"
,
skills
:
"
\
u80fd
\
u529b"
,
experts
:
"
\
u4e13
\
u5bb6"
,
plugins
:
"
\
u63d2
\
u4ef6"
,
settings
:
"
\
u8bbe
\
u7f6e"
,
bound
:
"
\
u5df2
\
u7ed1
\
u5b9a"
,
...
...
@@ -179,11 +211,16 @@ const ui = {
bindNow
:
"
\
u7acb
\
u5373
\
u7ed1
\
u5b9a"
,
binding
:
"
\
u7ed1
\
u5b9a
\
u4e2d..."
,
changeApiKey
:
"
\
u66f4
\
u6362
\
u5458
\
u5de5
\
u5bc6
\
u94a5"
,
skillChoice
:
"
\
u9009
\
u62e9
\
u
6280
\
u80fd
"
,
clearSkill
:
"
\
u6e05
\
u7a7a
\
u
6280
\
u80fd
"
,
skillMenuTitle
:
"
\
u9009
\
u62e9
\
u
6280
\
u80fd
"
,
skillChoice
:
"
\
u9009
\
u62e9
\
u
80fd
\
u529b
"
,
clearSkill
:
"
\
u6e05
\
u7a7a
\
u
80fd
\
u529b
"
,
skillMenuTitle
:
"
\
u9009
\
u62e9
\
u
80fd
\
u529b
"
,
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"
,
send
:
"
\
u53d1
\
u9001"
,
sending
:
"
\
u53d1
\
u9001
\
u4e2d..."
,
...
...
@@ -234,13 +271,13 @@ const ui = {
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"
,
chatNotReadyError
:
"
\
u5f53
\
u524d
\
u804a
\
u5929
\
u6682
\
u4e0d
\
u53ef
\
u7528
\
uff0c
\
u8bf7
\
u68c0
\
u67e5
\
u8fd0
\
u884c
\
u65f6
\
u72b6
\
u6001
\
u3002"
,
noSkillCards
:
"
\
u5f53
\
u524d
\
u
6ca1
\
u6709
\
u53ef
\
u7528
\
u6280
\
u80fd
\
u3002"
,
noSkillCards
:
"
\
u5f53
\
u524d
\
u
4e13
\
u5bb6
\
u8fd8
\
u6ca1
\
u6709
\
u53ef
\
u7528
\
u80fd
\
u529b
\
u3002"
,
pluginTitle
:
"
\
u63d2
\
u4ef6
\
u5217
\
u8868"
,
noPlugins
:
"
\
u5f53
\
u524d
\
u6ca1
\
u6709
\
u53ef
\
u7528
\
u63d2
\
u4ef6
\
u3002"
,
settingsTitle
:
"
\
u8bbe
\
u7f6e"
,
settingsDesc
:
"
\
u914d
\
u7f6e
\
u8fd0
\
u884c
\
u65f6
\
u3001
\
u5bc6
\
u94a5
\
u548c
\
u5de5
\
u4f5c
\
u76ee
\
u5f55
\
u3002"
,
chatPageDesc
:
"
\
u5728
\
u8fd9
\
u91cc
\
u4e0e
\
u
5343
\
u5320Claw
\
u5b8c
\
u6210
\
u5bf9
\
u8bdd
\
u548c
\
u534f
\
u4f5c
\
u3002"
,
skillsPageDesc
:
"
\
u67e5
\
u770b
\
u5f53
\
u524d
\
u
5df2
\
u51c6
\
u5907
\
u597d
\
u7684
\
u6280
\
u80fd
\
u80fd
\
u529b
\
u3002"
,
chatPageDesc
:
"
\
u5728
\
u8fd9
\
u91cc
\
u4e0e
\
u
4e13
\
u5bb6
\
u5b8c
\
u6210
\
u5bf9
\
u8bdd
\
u548c
\
u534f
\
u4f5c
\
u3002"
,
skillsPageDesc
:
"
\
u67e5
\
u770b
\
u5f53
\
u524d
\
u
4e13
\
u5bb6
\
u5df2
\
u51c6
\
u5907
\
u597d
\
u7684
\
u80fd
\
u529b
\
u3002"
,
pluginsPageDesc
:
"
\
u67e5
\
u770b
\
u5f53
\
u524d
\
u5df2
\
u542f
\
u7528
\
u7684
\
u63d2
\
u4ef6
\
u80fd
\
u529b
\
u3002"
,
workspacePath
:
"
\
u5de5
\
u4f5c
\
u76ee
\
u5f55"
,
save
:
"
\
u4fdd
\
u5b58"
,
...
...
@@ -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"
}
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
>
();
function
emitMockChatStreamEvent
(
event
:
ChatStreamEvent
)
{
...
...
@@ -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
=
{
workspace
:
{
getSummary
:
async
()
=>
({
...
...
@@ -311,6 +375,13 @@ const mockDesktopApi = {
runtimeCloudState
:
"ready"
,
runtimeState
:
"running"
,
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
,
skills
:
[
{
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 = {
credits
:
{
getSummary
:
async
()
=>
({
balance
:
0
,
granted
:
0
,
used
:
0
,
currency
:
"credits"
,
status
:
"ok"
,
updatedAt
:
new
Date
().
toISOString
()
})
},
skills
:
{
list
:
async
()
=>
[]
},
projects
:
{
list
:
async
()
=>
([
{
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
}
]),
list
:
async
()
=>
mockProjects
,
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"
})
},
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
:
{
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
()
}),
closeSession
:
async
()
=>
([{
id
:
"project:xiaohongshu:default"
,
projectId
:
"xiaohongshu"
,
title
:
ui
.
defaultChat
,
updatedAt
:
new
Date
().
toISOString
()
}])
,
listMessages
:
async
()
=>
[
{
id
:
"message-1"
,
role
:
"assistant"
,
content
:
"Mock UI active."
,
createdAt
:
new
Date
().
toISOString
()
}
],
closeSession
:
async
()
=>
mockSessions
,
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"
}
}),
streamPrompt
:
async
(
_sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
)
=>
{
const
requestId
=
createClientMessageId
(
"mock-request"
);
...
...
@@ -458,20 +526,111 @@ function getPluginCopy(plugin: WorkspaceSummary["plugins"][number]) {
return
pluginDisplayMap
[
plugin
.
id
]
??
{
name
:
plugin
.
name
,
description
:
plugin
.
description
};
}
function
getSkillStatusText
(
skill
:
WorkspaceSummary
[
"skills"
][
number
])
{
switch
(
skill
.
downloadState
)
{
case
"ready"
:
return
"Ready"
;
case
"downloading"
:
return
"Syncing"
;
case
"failed"
:
return
skill
.
lastError
?
`Failed:
${
skill
.
lastError
}
`
:
"Failed"
;
case
"pending"
:
return
"Pending"
;
case
"removed"
:
return
"Removed"
;
type
ExpertProject
=
WorkspaceSummary
[
"projects"
][
number
];
interface
ExpertGuideContent
{
greeting
:
string
;
summary
:
string
;
prompts
:
string
[];
}
function
getProjectDisplayName
(
project
:
ExpertProject
|
undefined
):
string
{
return
project
?.
displayName
??
project
?.
name
??
ui
.
defaultChat
;
}
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:
return
"Unknown"
;
return {
greeting: "说清你的目标,我会先帮你拆成可执行步骤。",
summary: "适合梳理需求、生成方案、组织信息和推进执行。",
prompts: [
"先帮我梳理这个任务的目标、输入和输出。",
"把我的需求拆成 3 个最先执行的步骤。",
"根据当前专家能力,给我一个最实用的处理方案。"
]
};
}
}
...
...
@@ -549,6 +708,7 @@ export default function App() {
const [errorText, setErrorText] = useState("");
const [infoText, setInfoText] = useState("");
const [messageTraces, setMessageTraces] = useState<Record<string, MessageTraceState>>({});
const [sidebarSessionTitles, setSidebarSessionTitles] = useState<Record<string, string>>({});
const [skillMenuOpen, setSkillMenuOpen] = useState(false);
const activeStreamRef = useRef<ActiveStreamState | null>(null);
const skillMenuRef = useRef<HTMLDivElement | null>(null);
...
...
@@ -571,6 +731,16 @@ export default function App() {
: startupCurtainCopy.loadingLabel;
const projects = workspace?.projects ?? [];
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 isBound = !setupRequired;
const hasActiveProject = Boolean(workspace?.projectReady && workspace?.currentProjectId);
...
...
@@ -588,8 +758,9 @@ export default function App() {
const setupActionDisabled = saving || !apiKeyDraft.trim() || (isDirectProviderSetup && (!baseUrlDraft.trim() || !defaultModelDraft.trim()));
const showBindEntry = !isBound && !showStartupOverlay;
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
pageDesc
=
viewMode
===
"chat"
?
ui
.
chatPageDesc
:
viewMode
===
"skills"
?
ui
.
skillsPageDesc
:
viewMode
===
"plugins"
?
ui
.
pluginsPageDesc
:
ui
.
settingsDesc
;
const isConversationView = viewMode === "chat" || viewMode === "experts";
const pageTitle = viewMode === "plugins" ? ui.plugins : ui.settings;
const pageDesc = viewMode === "plugins" ? ui.pluginsPageDesc : ui.settingsDesc;
useEffect(() => {
if (!infoText) {
return;
...
...
@@ -751,6 +922,18 @@ export default function App() {
};
}, [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(() => {
if (!config) {
return;
...
...
@@ -770,6 +953,42 @@ export default function App() {
void loadMessages(resolvedActiveSessionId, true, false);
}, [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) {
if (projectActionPending) {
return;
...
...
@@ -1488,16 +1707,46 @@ export default function App() {
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) {
setSelectedSkillId(skillId);
setSkillMenuOpen(false);
}
function applyStarterPrompt(nextPrompt: string) {
setViewMode((current) => (current === "experts" ? "experts" : "chat"));
setPrompt(nextPrompt);
}
function clearSelectedSkill() {
setSelectedSkillId(DEFAULT_SKILL.id);
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() {
setErrorText("");
await desktopApi.workspace.warmup().catch(() => undefined);
...
...
@@ -1516,81 +1765,31 @@ export default function App() {
}
}
return
(
<
div
className=
"shell"
>
<
aside
className=
"sidebar"
>
<
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
)
=>
(
const sidebarSessionLabel = "会话管理";
const selectedSkillBadge = selectedSkillId === DEFAULT_SKILL.id ? "自由对话" : "@" + selectedSkill.name;
const panelNewSessionAction = (
<button
key=
{
project
.
id
}
type="button"
className=
{
"project-chip"
+
(
workspace
?.
currentProjectId
===
project
.
id
?
" active"
:
""
)
}
disabled=
{
projectActionPending
}
onClick=
{
()
=>
void
switchProject
(
project
.
id
)
}
className="secondary conversation-new-session"
disabled={projectActionPending || !isBound || !projects.length
}
onClick={() => void createProjectSession(
)}
>
{
project
.
name
}
新建对话
</button>
))
}
</
div
>
</
div
>
)
:
null
}
{
!
showBindEntry
&&
sessions
.
length
>
0
?
(
<
div
className=
"session-strip"
>
<
div
className=
"session-strip-head"
>
<
span
>
{
ui
.
projectSessionsLabel
}
</
span
>
<
button
type=
"button"
className=
"session-add"
disabled=
{
projectActionPending
||
!
isBound
||
!
projects
.
length
}
onClick=
{
()
=>
void
createProjectSession
()
}
>
+
{
ui
.
newSession
}
</
button
>
</
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
>
);
const conversationPanelTitle = viewMode === "experts" ? activeExpertName : homeChatCopy.title;
const conversationPanelLead = viewMode === "chat" ? (
<div className="home-microcopy" aria-label="start your idea">
<span className="home-microcopy-icon">
<LobsterClawIcon />
</span>
<span className="home-microcopy-text">{homeChatCopy.microcopy}</span>
<span className="home-microcopy-tag">{selectedSkillBadge}</span>
</div>
)
:
null
}
{
showBindEntry
?
(
) : (
<div className="conversation-panel-kicker">{conversationPanelTitle}</div>
);
const bindEntryContent = (
<div className="bind-entry">
<div className="bind-entry-copy">
<strong>{ui.bindTitle}</strong>
...
...
@@ -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>
</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">
{messages.map((message) => {
const showThinking = message.role === "assistant" && message.streamState === "streaming" && !message.content.trim();
...
...
@@ -1610,7 +1834,7 @@ export default function App() {
const isTraceExpanded = Boolean(messageTrace?.expanded);
return (
<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 ? (
<div className="thinking-indicator" aria-live="polite">
<span className="thinking-spinner" aria-hidden="true" />
...
...
@@ -1640,14 +1864,29 @@ export default function App() {
) : null}
</div>
) : null}
</div>
</article>
);
})}
{
!
messages
.
length
?
<
div
className=
"empty-state"
>
{
ui
.
noMessages
}
</
div
>
:
null
}
{!messages.length && !showBindEntry ? activeEmptyState
: null}
</div>
);
const conversationBodyContent = showBindEntry
? bindEntryContent
: viewMode === "experts" && !expertPageProjects.length
? <div className="empty-state">{expertsPageCopy.noExperts}</div>
: messageListContent;
const composerContent = (
<div className="composer-shell">
<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>
<div className="composer-footer">
<div className="composer-left-tools" ref={skillMenuRef}>
...
...
@@ -1680,13 +1919,109 @@ export default function App() {
</div>
) : null}
</div>
<
button
disabled=
{
!
canSend
}
onClick=
{
()
=>
void
sendPrompt
()
}
>
{
sendButtonLabel
}
</
button
>
<button className="composer-submit"
disabled={!canSend} onClick={() => void sendPrompt()}>{sendButtonLabel}</button>
</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>
) : 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 === "settings" ? (
<div className="page-stack">
...
...
@@ -1825,44 +2160,3 @@ export default function App() {
</div>
);
}
apps/ui/src/styles.css
View file @
5e8d2c51
...
...
@@ -147,6 +147,219 @@ strong { font-weight: 600; }
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
,
1
fr
)
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
,
1
fr
));
}
.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
,
1
fr
));
}
.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
{
height
:
100vh
;
min-height
:
0
;
...
...
@@ -479,8 +692,8 @@ strong { font-weight: 600; }
}
.message-card
{
padding
:
16px
;
}
.message-card.user
{
background
:
#eef5ff
;
}
.message-card.assistant
{
background
:
#eefbf7
;
}
.message-card.user
{
background
:
transparent
;
}
.message-card.assistant
{
background
:
transparent
;
}
.message-card.streaming
{
border-color
:
#b7e4d5
;
}
.message-card.error
{
border-color
:
rgba
(
239
,
68
,
68
,
0.24
);
}
.message-card
p
{
...
...
@@ -713,8 +926,8 @@ strong { font-weight: 600; }
}
.composer-shell
{
gap
:
1
2
px
;
padding
:
1
4
px
;
gap
:
1
0
px
;
padding
:
1
2
px
;
border-radius
:
16px
;
border
:
1px
solid
#dbe5f1
;
background
:
linear-gradient
(
180deg
,
rgba
(
248
,
251
,
255
,
0.98
),
rgba
(
255
,
255
,
255
,
0.98
));
...
...
@@ -726,7 +939,7 @@ strong { font-weight: 600; }
}
.composer-field
textarea
{
min-height
:
124
px
;
min-height
:
88
px
;
}
.composer-footer
{
...
...
@@ -893,6 +1106,10 @@ strong { font-weight: 600; }
border-right
:
0
;
border-bottom
:
1px
solid
#dee6f1
;
}
.sidebar-bottom
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
overflow
:
visible
;
}
.main-shell
{
height
:
100%
;
}
...
...
@@ -924,7 +1141,9 @@ strong { font-weight: 600; }
@media
(
prefers-reduced-motion
:
reduce
)
{
.startup-overlay-panel
,
.startup-overlay-progress
span
,
.toast-notice
{
.toast-notice
,
.experts-drawer
,
.button-chevron
{
animation
:
none
;
transition
:
none
;
}
...
...
@@ -1021,3 +1240,1274 @@ strong { font-weight: 600; }
padding
:
0
;
color
:
#0a4f9d
;
}
@media
(
max-width
:
1100px
)
{
.chat-header
{
align-items
:
stretch
;
flex-direction
:
column
;
}
.chat-header-meta
,
.starter-prompt-list
{
grid-template-columns
:
1
fr
;
}
}
@media
(
max-width
:
720px
)
{
.sidebar-section-head
{
align-items
:
stretch
;
flex-direction
:
column
;
}
.sidebar-bottom
,
.starter-prompt-list
{
grid-template-columns
:
1
fr
;
}
}
.sidebar
{
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
}
.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
:
1
fr
;
}
.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
:
1
fr
;
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
:
1
fr
;
}
.expert-chip-list
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
.expert-chip-list.preview
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
}
@media
(
max-width
:
720px
)
{
.expert-chip-list
{
grid-template-columns
:
1
fr
;
}
.expert-chip-list.preview
{
grid-template-columns
:
1
fr
;
}
.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
,
1
fr
);
}
.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
,
1
fr
);
}
.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
,
1
fr
)
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
:
1
fr
;
}
.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
,
1
fr
)
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%
;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment