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
29bb5e3a
Commit
29bb5e3a
authored
Mar 25, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
UI交互修改 v1.0
parent
3815629f
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
371 additions
and
91 deletions
+371
-91
create-window.ts
apps/desktop/src/main/create-window.ts
+4
-3
App.tsx
apps/ui/src/App.tsx
+147
-51
styles.css
apps/ui/src/styles.css
+220
-37
No files found.
apps/desktop/src/main/create-window.ts
View file @
29bb5e3a
import
{
BrowserWindow
,
app
}
from
"electron"
;
import
{
BrowserWindow
,
app
}
from
"electron"
;
import
path
from
"node:path"
;
function
resolveRendererEntry
():
string
{
...
...
@@ -14,8 +14,8 @@ export function createMainWindow(smokeEnabled = false): BrowserWindow {
const
window
=
new
BrowserWindow
({
width
:
1400
,
height
:
920
,
minWidth
:
108
0
,
minHeight
:
72
0
,
minWidth
:
96
0
,
minHeight
:
64
0
,
backgroundColor
:
"#0f172a"
,
webPreferences
:
{
additionalArguments
:
smokeEnabled
?
[
"--qjc-smoke"
]
:
[],
...
...
@@ -35,3 +35,4 @@ export function createMainWindow(smokeEnabled = false): BrowserWindow {
return
window
;
}
apps/ui/src/App.tsx
View file @
29bb5e3a
...
...
@@ -21,6 +21,7 @@ type ViewMode = "chat" | "skills" | "plugins" | "settings";
type
Tone
=
"positive"
|
"warning"
;
const
DEFAULT_SESSION_ID
=
"desktop-main"
;
const
SUCCESS_NOTICE_TIMEOUT_MS
=
2400
;
const
DEFAULT_SKILL
=
{
id
:
"default-chat"
,
name
:
"默认对话"
,
...
...
@@ -30,16 +31,16 @@ const DEFAULT_SKILL = {
};
const
ui
=
{
app
:
"
钱江爪
"
,
app
:
"
千匠Claw
"
,
subtitle
:
"OpenClaw Client"
,
appDesc
:
"绑定 api_key 后自动配置运行时"
,
appDesc
:
"绑定 api_key 后自动拉取运行时配置"
,
heroLine
:
"千匠Claw,您身边最得力的员工,Start Your Ideas...."
,
chat
:
"对话"
,
skills
:
"技能"
,
plugins
:
"插件"
,
settings
:
"设置"
,
bound
:
"已绑定"
,
unbound
:
"未绑定"
,
preparing
:
"准备中"
,
defaultChat
:
"默认对话"
,
bindTitle
:
"绑定员工密钥"
,
bindDesc
:
"输入 OpenClaw employee api_key 后,桌面端会自动拉取运行时配置。"
,
...
...
@@ -48,17 +49,16 @@ const ui = {
bindNow
:
"立即绑定"
,
binding
:
"绑定中..."
,
changeApiKey
:
"更换员工密钥"
,
bindingManagedInSettings
:
"员工密钥已在设置中管理"
,
skillChoice
:
"选择技能"
,
noMessages
:
"当前没有消息,请先发送一条消息。"
,
taskPlaceholder
:
"输入消息后回车或点击发送"
,
taskDisabledPlaceholder
:
"请先绑定员工密钥后开始对话。"
,
send
:
"发送"
,
sending
:
"发送中..."
,
bindFirst
:
"请先绑定"
,
bindFirstError
:
"请先绑定员工密钥后再发送消息。"
,
refresh
:
"刷新"
,
startingHint
:
"运行时正在启动,请稍候。"
,
chatNotReadyError
:
"当前聊天不可用,请检查运行时状态。"
,
chatNotReadyError
:
"当前聊天
暂
不可用,请检查运行时状态。"
,
noSkillCards
:
"当前没有可用技能。"
,
pluginTitle
:
"插件列表"
,
noPlugins
:
"当前没有可用插件。"
,
...
...
@@ -86,7 +86,7 @@ const pluginDisplayMap: Record<string, { name: string; description: string }> =
"runtime-diagnostics"
:
{
name
:
"运行时诊断"
,
description
:
"查看运行时信息、日志和状态。"
},
"browser-automation"
:
{
name
:
"网页自动化"
,
description
:
"自动执行网页浏览、点击和表单操作。"
},
"browser-plugin"
:
{
name
:
"网页自动化"
,
description
:
"自动执行网页浏览、点击和表单操作。"
},
"ocr-tools"
:
{
name
:
"OCR 识别"
,
description
:
"识别扫描件和图片文字并提取结
果
。"
}
"ocr-tools"
:
{
name
:
"OCR 识别"
,
description
:
"识别扫描件和图片文字并提取结
构
。"
}
};
const
mockDesktopApi
=
{
...
...
@@ -166,12 +166,32 @@ const smokeEnabled = window.qjcSmokeEnabled === true;
declare
global
{
interface
Window
{
qjcDesktop
?:
DesktopApi
;
qjcSmokeEnabled
?:
boolean
;
__QJC_SMOKE__
?:
{
usingMockApi
:
boolean
;
gatewayStatus
:
GatewayStatus
|
null
;
gatewayHealth
:
GatewayHealth
|
null
;
runtimeStatus
:
RuntimeStatus
|
null
;
runtimeCloudStatus
:
RuntimeCloudStatus
|
null
;
runtimeTelemetry
:
RuntimeTelemetryStatus
|
null
;
config
:
AppConfig
|
null
;
authSession
:
null
;
profile
:
null
;
credits
:
null
;
skills
:
WorkspaceSummary
[
"skills"
];
modelConfig
:
null
;
systemSummary
:
SystemSummary
|
null
;
sessions
:
SessionSummary
[];
messages
:
ChatMessage
[];
logs
:
LogEntry
[];
activeSessionId
:
string
;
workspaceSummary
:
WorkspaceSummary
|
null
;
};
__QJC_SMOKE__
?:
{
usingMockApi
:
boolean
;
gatewayStatus
:
GatewayStatus
|
null
;
gatewayHealth
:
GatewayHealth
|
null
;
runtimeStatus
:
RuntimeStatus
|
null
;
runtimeCloudStatus
:
RuntimeCloudStatus
|
null
;
runtimeTelemetry
:
RuntimeTelemetryStatus
|
null
;
config
:
AppConfig
|
null
;
authSession
:
null
;
profile
:
null
;
credits
:
null
;
skills
:
WorkspaceSummary
[
"skills"
];
modelConfig
:
null
;
systemSummary
:
SystemSummary
|
null
;
sessions
:
SessionSummary
[];
messages
:
ChatMessage
[];
logs
:
LogEntry
[];
activeSessionId
:
string
;
workspaceSummary
:
WorkspaceSummary
|
null
;
};
}
}
const
err
=
(
value
:
unknown
)
=>
value
instanceof
Error
?
value
.
message
:
String
(
value
);
const
err
=
(
value
:
unknown
)
=>
(
value
instanceof
Error
?
value
.
message
:
String
(
value
)
);
function
StatusChip
({
tone
,
children
}:
{
tone
:
Tone
;
children
:
string
})
{
return
<
span
className=
{
"status-chip "
+
tone
}
>
{
children
}
</
span
>;
...
...
@@ -210,22 +230,37 @@ export default function App() {
const
[
errorText
,
setErrorText
]
=
useState
(
""
);
const
[
infoText
,
setInfoText
]
=
useState
(
""
);
const
effectiveSkills
=
useMemo
(()
=>
{
return
workspace
?.
skills
?.
length
?
workspace
.
skills
:
[
DEFAULT_SKILL
];
},
[
workspace
]);
const
effectiveSkills
=
useMemo
(()
=>
(
workspace
?.
skills
?.
length
?
workspace
.
skills
:
[
DEFAULT_SKILL
]),
[
workspace
]);
const
selectedSkill
=
useMemo
(()
=>
effectiveSkills
.
find
((
skill
)
=>
skill
.
id
===
selectedSkillId
)
??
effectiveSkills
[
0
]
??
DEFAULT_SKILL
,
[
effectiveSkills
,
selectedSkillId
]);
const
chatLaunchState
:
ChatLaunchState
=
workspace
?.
chatLaunchState
??
(
workspace
?.
apiKeyConfigured
?
"starting"
:
"unbound"
);
const
chatStatusMessage
=
workspace
?.
chatStatusMessage
??
(
chatLaunchState
===
"starting"
?
ui
.
startingHint
:
chatLaunchState
===
"error"
?
ui
.
chatNotReadyError
:
""
);
const
sessions
=
workspace
?.
apiKeyConfigured
?
[{
id
:
activeSessionId
,
title
:
ui
.
defaultChat
,
updatedAt
:
new
Date
().
toISOString
()
}]
:
[];
const
canSend
=
Boolean
(
workspace
?.
apiKeyConfigured
)
&&
prompt
.
trim
().
length
>
0
&&
!
sending
&&
!
saving
;
const
sendButtonLabel
=
sending
?
ui
.
sending
:
!
workspace
?.
apiKeyConfigured
?
ui
.
bindFirst
:
ui
.
send
;
const
isBound
=
Boolean
(
workspace
?.
apiKeyConfigured
);
const
canSend
=
isBound
&&
prompt
.
trim
().
length
>
0
&&
!
sending
&&
!
saving
;
const
sendButtonLabel
=
sending
?
ui
.
sending
:
!
isBound
?
ui
.
bindFirst
:
ui
.
send
;
const
showBindEntry
=
!
isBound
;
const
showChatStatusHint
=
isBound
&&
chatLaunchState
!==
"ready"
&&
Boolean
(
chatStatusMessage
);
const
pageTitle
=
viewMode
===
"chat"
?
ui
.
chat
:
viewMode
===
"skills"
?
ui
.
skills
:
viewMode
===
"plugins"
?
ui
.
plugins
:
ui
.
settings
;
const
pageDesc
=
viewMode
===
"chat"
?
"查看聊天、技能和运行状态。"
:
viewMode
===
"skills"
?
"查看当前可用技能。"
:
viewMode
===
"plugins"
?
"查看已集成插件。"
:
ui
.
settingsDesc
;
useEffect
(()
=>
{
if
(
!
infoText
)
{
return
;
}
const
timer
=
window
.
setTimeout
(()
=>
{
setInfoText
(
""
);
},
SUCCESS_NOTICE_TIMEOUT_MS
);
return
()
=>
window
.
clearTimeout
(
timer
);
},
[
infoText
]);
async
function
loadMessages
(
sessionId
:
string
,
canRead
:
boolean
,
showError
=
false
)
{
if
(
!
canRead
)
{
setMessages
([]);
return
;
}
try
{
setMessages
(
await
desktopApi
.
chat
.
listMessages
(
sessionId
));
}
catch
(
error
)
{
...
...
@@ -239,6 +274,7 @@ export default function App() {
async
function
refresh
()
{
setRefreshing
(
true
);
setErrorText
(
""
);
try
{
const
[
nextConfig
,
initialRuntime
,
nextCloud
,
nextTelemetry
,
nextSystem
]
=
await
Promise
.
all
([
desktopApi
.
config
.
load
(),
...
...
@@ -297,9 +333,11 @@ export default function App() {
if
(
workspace
?.
chatLaunchState
!==
"starting"
)
{
return
;
}
const
timer
=
window
.
setTimeout
(()
=>
{
void
refresh
();
},
2000
);
return
()
=>
window
.
clearTimeout
(
timer
);
},
[
workspace
?.
chatLaunchState
]);
...
...
@@ -314,24 +352,57 @@ export default function App() {
delete
window
.
__QJC_SMOKE__
;
return
;
}
window
.
__QJC_SMOKE__
=
{
usingMockApi
:
isMockDesktopApi
,
gatewayStatus
,
gatewayHealth
,
runtimeStatus
,
runtimeCloudStatus
,
runtimeTelemetry
,
config
,
authSession
:
null
,
profile
:
null
,
credits
:
null
,
skills
:
workspace
?.
skills
??
[],
modelConfig
:
null
,
systemSummary
,
sessions
,
messages
,
logs
:
[],
activeSessionId
,
workspaceSummary
:
workspace
};
window
.
__QJC_SMOKE__
=
{
usingMockApi
:
isMockDesktopApi
,
gatewayStatus
,
gatewayHealth
,
runtimeStatus
,
runtimeCloudStatus
,
runtimeTelemetry
,
config
,
authSession
:
null
,
profile
:
null
,
credits
:
null
,
skills
:
workspace
?.
skills
??
[],
modelConfig
:
null
,
systemSummary
,
sessions
,
messages
,
logs
:
[],
activeSessionId
,
workspaceSummary
:
workspace
};
},
[
activeSessionId
,
config
,
gatewayHealth
,
gatewayStatus
,
messages
,
runtimeCloudStatus
,
runtimeStatus
,
runtimeTelemetry
,
sessions
,
systemSummary
,
workspace
]);
async
function
saveConfig
(
nextApiKey
?:
string
)
{
if
(
!
config
)
{
return
;
}
setSaving
(
true
);
setErrorText
(
""
);
setInfoText
(
""
);
try
{
const
trimmedApiKey
=
nextApiKey
?.
trim
();
const
input
:
SaveConfigInput
=
{
provider
:
config
.
provider
,
baseUrl
:
config
.
baseUrl
,
defaultModel
:
config
.
defaultModel
,
workspacePath
:
workspacePathDraft
.
trim
()
||
config
.
workspacePath
,
gatewayUrl
:
config
.
gatewayUrl
,
cloudApiBaseUrl
:
config
.
cloudApiBaseUrl
,
runtimeCloudApiBaseUrl
:
config
.
runtimeCloudApiBaseUrl
,
runtimeMode
:
"bundled-runtime"
,
...(
trimmedApiKey
?
{
apiKey
:
trimmedApiKey
}
:
{})
};
const
input
:
SaveConfigInput
=
{
provider
:
config
.
provider
,
baseUrl
:
config
.
baseUrl
,
defaultModel
:
config
.
defaultModel
,
workspacePath
:
workspacePathDraft
.
trim
()
||
config
.
workspacePath
,
gatewayUrl
:
config
.
gatewayUrl
,
cloudApiBaseUrl
:
config
.
cloudApiBaseUrl
,
runtimeCloudApiBaseUrl
:
config
.
runtimeCloudApiBaseUrl
,
runtimeMode
:
"bundled-runtime"
,
...(
trimmedApiKey
?
{
apiKey
:
trimmedApiKey
}
:
{})
};
const
savedConfig
=
await
desktopApi
.
config
.
save
(
input
);
setConfig
(
savedConfig
);
setWorkspacePathDraft
(
savedConfig
.
workspacePath
);
setApiKeyDraft
(
""
);
setInfoText
(
trimmedApiKey
?
"员工密钥已保存。"
:
"已清除员工密钥。"
);
setInfoText
(
trimmedApiKey
?
"员工密钥已保存。"
:
"已清除员工密钥。"
);
await
refresh
();
}
catch
(
error
)
{
setErrorText
(
err
(
error
));
...
...
@@ -398,12 +469,15 @@ export default function App() {
if
(
!
canSend
)
{
return
;
}
setSending
(
true
);
setErrorText
(
""
);
try
{
await
ensureChatAvailable
();
const
skillId
=
selectedSkill
.
id
===
DEFAULT_SKILL
.
id
?
undefined
:
selectedSkill
.
id
;
const
result
=
await
desktopApi
.
chat
.
sendPrompt
(
DEFAULT_SESSION_ID
,
prompt
.
trim
(),
skillId
);
setPrompt
(
""
);
setActiveSessionId
(
result
.
sessionId
);
await
loadMessages
(
result
.
sessionId
,
true
,
true
);
...
...
@@ -430,6 +504,7 @@ export default function App() {
async
function
exportDiagnostics
()
{
setErrorText
(
""
);
setInfoText
(
""
);
try
{
const
result
=
await
desktopApi
.
diagnostics
.
exportSnapshot
();
setInfoText
(
ui
.
exported
+
result
.
filePath
);
...
...
@@ -438,9 +513,6 @@ export default function App() {
}
}
const
pageTitle
=
viewMode
===
"chat"
?
ui
.
chat
:
viewMode
===
"skills"
?
ui
.
skills
:
viewMode
===
"plugins"
?
ui
.
plugins
:
ui
.
settings
;
const
pageDesc
=
viewMode
===
"chat"
?
"查看聊天、技能和运行状态。"
:
viewMode
===
"skills"
?
"查看当前可用技能。"
:
viewMode
===
"plugins"
?
"查看已集成插件。"
:
ui
.
settingsDesc
;
return
(
<
div
className=
"shell"
>
<
aside
className=
"sidebar"
>
...
...
@@ -456,45 +528,69 @@ export default function App() {
</
nav
>
</
aside
>
<
div
className=
"main-shell"
>
<
header
className=
"page-header panel"
>
<
div
><
h2
>
{
pageTitle
}
</
h2
><
p
>
{
pageDesc
}
</
p
></
div
>
<
div
className=
"header-actions"
>
{
isMockDesktopApi
?
<
StatusChip
tone=
"warning"
>
Mock API
</
StatusChip
>
:
null
}
<
button
className=
"secondary"
disabled=
{
refreshing
||
saving
}
onClick=
{
()
=>
void
refresh
()
}
>
{
refreshing
?
ui
.
preparing
:
ui
.
refresh
}
</
button
></
div
>
</
header
>
{
infoText
?
<
div
className=
"notice"
>
{
infoText
}
</
div
>
:
null
}
<
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"
>
<
div
className=
"chat-topbar"
>
<
div
className=
"bind-
block
"
>
<
div
className=
"
section-head compact
"
>
<
div
><
h3
>
{
ui
.
bindTitle
}
</
h3
><
p
>
{
ui
.
bindDesc
}
</
p
></
div
>
<
StatusChip
tone=
{
workspace
?.
apiKeyConfigured
?
"positive"
:
"warning"
}
>
{
workspace
?.
apiKeyConfigured
?
ui
.
bound
:
ui
.
unbound
}
</
StatusChi
p
>
{
showBindEntry
?
(
<
div
className=
"bind-
entry
"
>
<
div
className=
"
bind-entry-copy
"
>
<
strong
>
{
ui
.
bindTitle
}
</
strong
>
<
p
>
{
ui
.
bindDesc
}
</
p
>
</
div
>
{
workspace
?.
apiKeyConfigured
?
(
<>
<
div
className=
"mini-info"
><
span
>
{
ui
.
currentBinding
}
</
span
><
strong
>
{
ui
.
bound
}
</
strong
></
div
>
<
div
className=
"inline-hint"
>
{
ui
.
bindingManagedInSettings
}
</
div
>
</>
)
:
(
<
div
className=
"bind-row"
>
<
input
type=
"password"
value=
{
apiKeyDraft
}
placeholder=
{
ui
.
apiKeyPlaceholder
}
onChange=
{
(
event
)
=>
setApiKeyDraft
(
event
.
target
.
value
)
}
/>
<
button
disabled=
{
saving
||
apiKeyDraft
.
trim
().
length
===
0
}
onClick=
{
()
=>
void
saveConfig
(
apiKeyDraft
)
}
>
{
saving
?
ui
.
binding
:
ui
.
bindNow
}
</
button
>
</
div
>
)
}
</
div
>
<
div
className=
"skill-block"
>
<
label
>
{
ui
.
skillChoice
}
<
select
value=
{
selectedSkillId
}
onChange=
{
(
event
)
=>
setSelectedSkillId
(
event
.
target
.
value
)
}
>
{
effectiveSkills
.
map
((
skill
)
=>
<
option
key=
{
skill
.
id
}
value=
{
skill
.
id
}
>
{
skill
.
name
}
</
option
>)
}
</
select
></
label
>
)
:
null
}
{
showChatStatusHint
?
<
div
className=
{
"inline-hint"
+
(
chatLaunchState
===
"error"
?
" error"
:
""
)
}
>
{
chatStatusMessage
}
</
div
>
:
null
}
<
div
className=
"message-list"
>
{
messages
.
map
((
message
)
=>
(
<
article
key=
{
message
.
id
}
className=
{
"message-card "
+
message
.
role
}
>
<
header
><
strong
>
{
message
.
role
===
"assistant"
?
ui
.
app
:
message
.
role
===
"user"
?
"用户"
:
"系统"
}
</
strong
></
header
>
<
p
>
{
message
.
content
}
</
p
>
</
article
>
))
}
{
!
messages
.
length
?
<
div
className=
"empty-state"
>
{
ui
.
noMessages
}
</
div
>
:
null
}
</
div
>
<
div
className=
"composer-shell"
>
<
div
className=
"composer-meta"
>
<
label
className=
"skill-select"
>
<
span
className=
"field-label"
>
{
ui
.
skillChoice
}
</
span
>
<
select
value=
{
selectedSkillId
}
disabled=
{
!
isBound
}
onChange=
{
(
event
)
=>
setSelectedSkillId
(
event
.
target
.
value
)
}
>
{
effectiveSkills
.
map
((
skill
)
=>
<
option
key=
{
skill
.
id
}
value=
{
skill
.
id
}
>
{
skill
.
name
}
</
option
>)
}
</
select
>
</
label
>
<
p
className=
"composer-hint"
>
{
isBound
?
selectedSkill
.
description
:
ui
.
taskDisabledPlaceholder
}
</
p
>
</
div
>
<
label
>
<
span
className=
"field-label"
>
{
selectedSkill
.
name
}
</
span
>
<
textarea
value=
{
prompt
}
disabled=
{
!
isBound
}
onChange=
{
(
event
)
=>
setPrompt
(
event
.
target
.
value
)
}
placeholder=
{
isBound
?
ui
.
taskPlaceholder
:
ui
.
taskDisabledPlaceholder
}
/>
</
label
>
<
div
className=
"button-row composer-actions"
>
<
button
disabled=
{
!
canSend
}
onClick=
{
()
=>
void
sendPrompt
()
}
>
{
sendButtonLabel
}
</
button
>
</
div
>
</
div
>
{
workspace
?.
apiKeyConfigured
&&
chatLaunchState
!==
"ready"
&&
chatStatusMessage
?
<
div
className=
{
"inline-hint"
+
(
chatLaunchState
===
"error"
?
" error"
:
""
)
}
>
{
chatStatusMessage
}
</
div
>
:
null
}
<
div
className=
"message-list"
>
{
messages
.
map
((
message
)
=>
<
article
key=
{
message
.
id
}
className=
{
"message-card "
+
message
.
role
}
><
header
><
strong
>
{
message
.
role
===
"assistant"
?
ui
.
app
:
message
.
role
===
"user"
?
"用户"
:
"系统"
}
</
strong
></
header
><
p
>
{
message
.
content
}
</
p
></
article
>)
}{
!
messages
.
length
?
<
div
className=
"empty-state"
>
{
ui
.
noMessages
}
</
div
>
:
null
}
</
div
>
<
label
><
span
className=
"field-label"
>
{
selectedSkill
.
name
}
</
span
><
textarea
value=
{
prompt
}
onChange=
{
(
event
)
=>
setPrompt
(
event
.
target
.
value
)
}
placeholder=
{
ui
.
taskPlaceholder
}
/></
label
>
<
div
className=
"button-row"
><
button
disabled=
{
!
canSend
}
onClick=
{
()
=>
void
sendPrompt
()
}
>
{
sendButtonLabel
}
</
button
><
button
className=
"secondary"
disabled=
{
refreshing
||
saving
}
onClick=
{
()
=>
void
refresh
()
}
>
{
ui
.
refresh
}
</
button
></
div
>
</
section
>
)
:
null
}
{
viewMode
===
"skills"
?
<
section
className=
"panel catalog-list"
>
{
effectiveSkills
.
map
((
skill
)
=>
<
button
key=
{
skill
.
id
}
type=
"button"
className=
"catalog-item"
onClick=
{
()
=>
{
setSelectedSkillId
(
skill
.
id
);
setViewMode
(
"chat"
);
}
}
><
strong
>
{
skill
.
name
}
</
strong
><
p
>
{
skill
.
description
}
</
p
></
button
>)
}{
!
effectiveSkills
.
length
?
<
div
className=
"empty-state"
>
{
ui
.
noSkillCards
}
</
div
>
:
null
}
</
section
>
:
null
}
{
viewMode
===
"plugins"
?
<
section
className=
"panel catalog-list"
><
div
className=
"section-head compact"
><
div
><
h3
>
{
ui
.
pluginTitle
}
</
h3
></
div
></
div
>
{
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
}
</
section
>
:
null
}
{
viewMode
===
"skills"
?
<
section
className=
"panel catalog-list"
>
<
div
className=
"scroll-panel"
>
{
effectiveSkills
.
map
((
skill
)
=>
<
button
key=
{
skill
.
id
}
type=
"button"
className=
"catalog-item"
onClick=
{
()
=>
{
setSelectedSkillId
(
skill
.
id
);
setViewMode
(
"chat"
);
}
}
><
strong
>
{
skill
.
name
}
</
strong
><
p
>
{
skill
.
description
}
</
p
></
button
>)
}{
!
effectiveSkills
.
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"
><
section
className=
"panel settings-panel"
><
div
className=
"section-head compact"
><
div
><
h3
>
{
ui
.
settingsTitle
}
</
h3
><
p
>
{
ui
.
settingsDesc
}
</
p
></
div
><
StatusChip
tone=
{
workspace
?.
apiKeyConfigured
?
"positive"
:
"warning"
}
>
{
workspace
?.
apiKeyConfigured
?
ui
.
bound
:
ui
.
unbound
}
</
StatusChip
></
div
><
div
className=
"form-grid single"
><
label
>
{
ui
.
apiKey
}
<
input
type=
"password"
value=
{
apiKeyDraft
}
placeholder=
{
workspace
?.
apiKeyConfigured
?
ui
.
changeApiKey
:
ui
.
apiKeyPlaceholder
}
onChange=
{
(
event
)
=>
setApiKeyDraft
(
event
.
target
.
value
)
}
/></
label
><
label
>
{
ui
.
workspacePath
}
<
input
value=
{
workspacePathDraft
}
onChange=
{
(
event
)
=>
setWorkspacePathDraft
(
event
.
target
.
value
)
}
/></
label
></
div
><
div
className=
"button-row"
><
button
disabled=
{
saving
}
onClick=
{
()
=>
void
saveConfig
(
apiKeyDraft
)
}
>
{
saving
?
ui
.
saving
:
ui
.
save
}
</
button
></
div
><
div
className=
"mini-info"
><
span
>
{
ui
.
currentBinding
}
</
span
><
strong
>
{
workspace
?.
apiKeyConfigured
?
ui
.
bound
:
ui
.
unbound
}
</
strong
></
div
></
section
><
section
className=
"panel settings-panel"
><
div
className=
"section-head compact"
><
div
><
h3
>
{
ui
.
diagnostics
}
</
h3
><
p
>
{
ui
.
diagnosticsDesc
}
</
p
></
div
></
div
><
div
className=
"mini-info"
><
span
>
{
ui
.
workspacePath
}
</
span
><
strong
>
{
config
?.
workspacePath
||
workspacePathDraft
||
ui
.
none
}
</
strong
></
div
><
div
className=
"button-row"
><
button
className=
"secondary"
onClick=
{
()
=>
void
exportDiagnostics
()
}
>
{
ui
.
export
}
</
button
></
div
></
section
></
div
>
:
null
}
</
main
>
</
div
>
...
...
apps/ui/src/styles.css
View file @
29bb5e3a
...
...
@@ -8,8 +8,14 @@
}
*
{
box-sizing
:
border-box
;
}
html
,
body
,
#root
{
margin
:
0
;
min-height
:
100%
;
}
body
{
min-height
:
100vh
;
}
html
,
body
,
#root
{
margin
:
0
;
height
:
100%
;
}
body
{
min-height
:
100vh
;
overflow
:
hidden
;
}
button
,
input
,
textarea
,
select
{
font
:
inherit
;
}
button
{
border
:
0
;
...
...
@@ -25,7 +31,13 @@ button.secondary {
color
:
#2d3955
;
box-shadow
:
inset
0
0
0
1px
#d8e1ef
;
}
button
:disabled
{
opacity
:
0.55
;
cursor
:
not-allowed
;
}
button
:disabled
,
input
:disabled
,
textarea
:disabled
,
select
:disabled
{
opacity
:
0.6
;
cursor
:
not-allowed
;
}
input
,
textarea
,
select
{
width
:
100%
;
border
:
1px
solid
#d9e2ef
;
...
...
@@ -34,27 +46,52 @@ input, textarea, select {
background
:
#fff
;
color
:
#182236
;
}
textarea
{
min-height
:
150px
;
resize
:
vertical
;
}
label
{
display
:
grid
;
gap
:
8px
;
color
:
#53637f
;
font-size
:
13px
;
}
textarea
{
min-height
:
128px
;
resize
:
vertical
;
max-height
:
220px
;
overflow
:
auto
;
overscroll-behavior
:
contain
;
}
label
{
display
:
grid
;
gap
:
8px
;
color
:
#53637f
;
font-size
:
13px
;
}
p
,
h1
,
h2
,
h3
,
strong
,
span
{
margin
:
0
;
}
strong
{
font-weight
:
600
;
}
.shell
{
height
:
100vh
;
min-height
:
100vh
;
display
:
grid
;
grid-template-columns
:
208px
minmax
(
0
,
1
fr
);
overflow
:
hidden
;
}
.sidebar
{
height
:
100vh
;
padding
:
22px
16px
;
display
:
grid
;
grid-template-rows
:
auto
1
fr
;
grid-template-rows
:
auto
auto
;
align-content
:
start
;
gap
:
14px
;
background
:
linear-gradient
(
180deg
,
#f8fbff
,
#edf3fa
);
border-right
:
1px
solid
#dee6f1
;
}
.brand-block
,
.nav-list
,
.page-stack
,
.content-area
,
.message-list
,
.catalog-list
,
.form-grid
,
.skill-picker
,
.chat-panel
,
.settings-panel
,
.bind-block
{
.brand-block
,
.nav-list
,
.page-stack
,
.content-area
,
.catalog-list
,
.form-grid
,
.chat-panel
,
.settings-panel
,
.bind-entry
,
.composer-shell
,
.scroll-panel
{
display
:
grid
;
gap
:
12px
;
}
...
...
@@ -71,13 +108,25 @@ strong { font-weight: 600; }
}
.brand-block
h1
{
font-size
:
28px
;
}
.brand-block
p
,
.page-header
p
,
.section-head
p
,
.catalog-item
p
,
.notice
,
.empty-state
,
.mini-info
span
,
.inline-hint
{
.brand-block
p
,
.page-copy
p
,
.catalog-item
p
,
.notice
,
.empty-state
,
.mini-info
span
,
.inline-hint
,
.bind-entry-copy
p
,
.composer-hint
{
color
:
#667794
;
line-height
:
1.6
;
font-size
:
13px
;
}
.nav-list
{
gap
:
8px
;
}
.nav-list
{
gap
:
8px
;
align-content
:
start
;
align-self
:
start
;
}
.nav-item
{
height
:
42px
;
padding
:
0
12px
;
...
...
@@ -97,49 +146,102 @@ strong { font-weight: 600; }
}
.main-shell
{
height
:
100vh
;
min-height
:
0
;
padding
:
22px
;
display
:
grid
;
display
:
flex
;
flex-direction
:
column
;
gap
:
14px
;
overflow
:
hidden
;
}
.panel
,
.notice
,
.empty-state
,
.message-card
,
.catalog-item
{
.panel
,
.notice
,
.empty-state
,
.message-card
,
.catalog-item
{
border-radius
:
18px
;
background
:
rgba
(
255
,
255
,
255
,
0.92
);
border
:
1px
solid
#dfe7f2
;
box-shadow
:
0
18px
40px
rgba
(
35
,
52
,
82
,
0.06
);
}
.panel
{
padding
:
20px
;
}
.page-header
{
padding
:
16px
20px
;
.panel
{
padding
:
18px
;
}
.page-topbar
,
.header-actions
,
.section-head
,
.button-row
,
.mini-info
,
.bind-row
,
.composer-meta
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
1
4
px
;
gap
:
1
2
px
;
}
.header-actions
,
.section-head
,
.button-row
,
.mini-info
,
.bind-row
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
10px
;
.page-topbar
{
align-items
:
flex-start
;
flex
:
0
0
auto
;
}
.page-copy
{
display
:
grid
;
gap
:
4px
;
}
.hero-line
{
font-size
:
24px
;
line-height
:
1.35
;
letter-spacing
:
0.01em
;
color
:
#182236
;
}
.section-head.compact
{
align-items
:
flex-start
;
}
.notice
,
.empty-state
,
.catalog-item
{
padding
:
14px
;
}
.notice
{
background
:
rgba
(
15
,
123
,
255
,
0.08
);
color
:
#28507f
;
}
.notice.error
{
background
:
rgba
(
239
,
68
,
68
,
0.08
);
color
:
#972f2f
;
}
.notice
{
background
:
rgba
(
15
,
123
,
255
,
0.08
);
color
:
#28507f
;
flex
:
0
0
auto
;
}
.notice.error
{
background
:
rgba
(
239
,
68
,
68
,
0.08
);
color
:
#972f2f
;
}
.toast-notice
{
animation
:
notice-fade
2.4s
ease
forwards
;
}
.empty-state
{
background
:
#f8fbff
;
border-style
:
dashed
;
}
.chat-topbar
{
.content-area
{
flex
:
1
1
auto
;
min-height
:
0
;
overflow
:
hidden
;
}
.chat-panel
{
height
:
100%
;
min-height
:
0
;
grid-template-rows
:
auto
auto
minmax
(
0
,
1
fr
)
auto
;
overflow
:
hidden
;
}
.bind-entry
{
padding
:
14px
;
border-radius
:
16px
;
border
:
1px
dashed
#cfdced
;
background
:
#f8fbff
;
}
.bind-entry-copy
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1.6
fr
)
minmax
(
220px
,
0.9
fr
);
gap
:
14px
;
align-items
:
start
;
gap
:
6px
;
}
.bind-row
input
{
flex
:
1
1
auto
;
}
.bind-row
button
{
flex
:
0
0
auto
;
min-width
:
88px
;
}
.bind-row
button
{
flex
:
0
0
auto
;
min-width
:
96px
;
}
.inline-hint
{
padding
:
10px
12px
;
border-radius
:
12px
;
...
...
@@ -153,16 +255,60 @@ strong { font-weight: 600; }
}
.field-label
{
color
:
#53637f
;
font-size
:
13px
;
}
.message-list
{
min-height
:
280px
;
max-height
:
480px
;
.message-list
,
.scroll-panel
,
.page-stack
{
min-height
:
0
;
overflow
:
auto
;
overscroll-behavior
:
contain
;
}
.message-list
{
padding-right
:
4px
;
display
:
grid
;
align-content
:
start
;
gap
:
12px
;
}
.scroll-panel
{
align-content
:
start
;
}
.message-card
{
padding
:
16px
;
}
.message-card.user
{
background
:
#eef5ff
;
}
.message-card.assistant
{
background
:
#eefbf7
;
}
.message-card
p
{
white-space
:
pre-wrap
;
line-height
:
1.7
;
}
.message-card
p
{
white-space
:
pre-wrap
;
line-height
:
1.7
;
margin-top
:
6px
;
}
.composer-shell
{
gap
:
10px
;
padding
:
14px
;
border-radius
:
16px
;
border
:
1px
solid
#dbe5f1
;
background
:
linear-gradient
(
180deg
,
rgba
(
248
,
251
,
255
,
0.98
),
rgba
(
255
,
255
,
255
,
0.98
));
flex
:
0
0
auto
;
}
.composer-meta
{
align-items
:
flex-end
;
}
.skill-select
{
min-width
:
220px
;
max-width
:
300px
;
}
.composer-hint
{
flex
:
1
1
auto
;
min-width
:
0
;
}
.composer-actions
{
justify-content
:
flex-end
;
}
.catalog-item
{
text-align
:
left
;
...
...
@@ -198,21 +344,58 @@ strong { font-weight: 600; }
.status-chip.positive
{
background
:
rgba
(
16
,
185
,
129
,
0.12
);
color
:
#0f7f59
;
}
.status-chip.warning
{
background
:
rgba
(
245
,
158
,
11
,
0.14
);
color
:
#b46f0a
;
}
@keyframes
notice-fade
{
0
%,
78
%
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
100
%
{
opacity
:
0
;
transform
:
translateY
(
-4px
);
}
}
@media
(
max-width
:
1100px
)
{
.hero-line
{
font-size
:
21px
;
}
.composer-meta
{
align-items
:
stretch
;
flex-direction
:
column
;
}
.skill-select
{
min-width
:
0
;
max-width
:
none
;
width
:
100%
;
}
}
@media
(
max-width
:
960px
)
{
.shell
{
grid-template-columns
:
1
fr
;
}
.shell
{
grid-template-columns
:
1
fr
;
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
}
.sidebar
{
height
:
auto
;
border-right
:
0
;
border-bottom
:
1px
solid
#dee6f1
;
}
.main-shell
{
height
:
100%
;
}
.nav-list
{
grid-template-columns
:
repeat
(
4
,
minmax
(
0
,
1
fr
));
}
.chat-topbar
{
grid-template-columns
:
1
fr
;
}
}
@media
(
max-width
:
720px
)
{
.main-shell
,
.sidebar
{
padding
:
16px
;
}
.page-header
,
.header-actions
,
.button-row
,
.mini-info
,
.bind-row
{
align-items
:
stretch
;
flex-direction
:
column
;
}
.page-topbar
,
.header-actions
,
.button-row
,
.mini-info
,
.bind-row
{
align-items
:
stretch
;
flex-direction
:
column
;
}
.nav-list
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
.nav-item
{
justify-content
:
center
;
}
.hero-line
{
font-size
:
18px
;
}
.chat-panel
{
padding
:
16px
;
}
}
\ No newline at end of file
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