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
f4e11f94
Commit
f4e11f94
authored
Apr 29, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): refine conversation workspace layout and expert sidebar
parent
6008eeb5
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
519 additions
and
169 deletions
+519
-169
App.tsx
apps/ui/src/App.tsx
+227
-48
styles.css
apps/ui/src/styles.css
+292
-121
No files found.
apps/ui/src/App.tsx
View file @
f4e11f94
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
;
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
;
import
brandIcon
from
"./assets/brand-icon.png"
;
import
brandIcon
from
"./assets/brand-icon.png"
;
import
type
{
ReactNode
,
C
hangeEvent
,
DragEvent
as
ReactDragEvent
,
KeyboardEvent
as
ReactKeyboard
Event
}
from
"react"
;
import
type
{
ReactNode
,
C
SSProperties
,
ChangeEvent
,
DragEvent
as
ReactDragEvent
,
KeyboardEvent
as
ReactKeyboardEvent
,
PointerEvent
as
ReactPointer
Event
}
from
"react"
;
import
type
{
import
type
{
AppConfig
,
AppConfig
,
ChatAttachment
,
ChatAttachment
,
...
@@ -138,6 +138,30 @@ const IMAGE_ATTACHMENT_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", "
...
@@ -138,6 +138,30 @@ const IMAGE_ATTACHMENT_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", "
const
DOCUMENT_ATTACHMENT_EXTENSIONS
=
new
Set
([
".pdf"
,
".ppt"
,
".pptx"
,
".xls"
,
".xlsx"
,
".csv"
,
".tsv"
,
".doc"
,
".docx"
,
".txt"
,
".md"
,
".json"
,
".mp3"
]);
const
DOCUMENT_ATTACHMENT_EXTENSIONS
=
new
Set
([
".pdf"
,
".ppt"
,
".pptx"
,
".xls"
,
".xlsx"
,
".csv"
,
".tsv"
,
".doc"
,
".docx"
,
".txt"
,
".md"
,
".json"
,
".mp3"
]);
const
SUPPORTED_ATTACHMENT_EXTENSIONS
=
new
Set
([...
IMAGE_ATTACHMENT_EXTENSIONS
,
...
DOCUMENT_ATTACHMENT_EXTENSIONS
]);
const
SUPPORTED_ATTACHMENT_EXTENSIONS
=
new
Set
([...
IMAGE_ATTACHMENT_EXTENSIONS
,
...
DOCUMENT_ATTACHMENT_EXTENSIONS
]);
const
COMPOSER_ATTACHMENT_ACCEPT
=
[...
SUPPORTED_ATTACHMENT_EXTENSIONS
].
join
(
","
);
const
COMPOSER_ATTACHMENT_ACCEPT
=
[...
SUPPORTED_ATTACHMENT_EXTENSIONS
].
join
(
","
);
const
COMPOSER_TEXTAREA_DEFAULT_MIN_HEIGHT
=
48
;
const
COMPOSER_TEXTAREA_SAFE_MIN_HEIGHT
=
38
;
const
COMPOSER_TEXTAREA_MAX_HEIGHT
=
188
;
const
COMPOSER_TEXTAREA_DEFAULT_RATIO
=
0.145
;
const
COMPOSER_TEXTAREA_MIN_RATIO
=
0.12
;
const
COMPOSER_TEXTAREA_MAX_RATIO
=
0.32
;
function
getComposerTextareaBounds
(
workspaceHeight
:
number
):
{
min
:
number
;
max
:
number
}
{
const
safeWorkspaceHeight
=
Number
.
isFinite
(
workspaceHeight
)
&&
workspaceHeight
>
0
?
workspaceHeight
:
0
;
const
minByWorkspace
=
safeWorkspaceHeight
>
0
?
Math
.
min
(
COMPOSER_TEXTAREA_DEFAULT_MIN_HEIGHT
,
safeWorkspaceHeight
*
COMPOSER_TEXTAREA_MIN_RATIO
)
:
COMPOSER_TEXTAREA_DEFAULT_MIN_HEIGHT
;
const
maxByWorkspace
=
safeWorkspaceHeight
*
COMPOSER_TEXTAREA_MAX_RATIO
;
const
dynamicMinHeight
=
Math
.
max
(
COMPOSER_TEXTAREA_SAFE_MIN_HEIGHT
,
minByWorkspace
);
return
{
min
:
dynamicMinHeight
,
max
:
Math
.
max
(
dynamicMinHeight
,
Math
.
min
(
COMPOSER_TEXTAREA_MAX_HEIGHT
,
maxByWorkspace
||
COMPOSER_TEXTAREA_MAX_HEIGHT
))
};
}
function
clampComposerTextareaHeight
(
height
:
number
,
workspaceHeight
:
number
):
number
{
const
bounds
=
getComposerTextareaBounds
(
workspaceHeight
);
return
Math
.
min
(
bounds
.
max
,
Math
.
max
(
bounds
.
min
,
height
));
}
function
shouldOfferHomeExpertSwitch
(
prompt
:
string
):
boolean
{
function
shouldOfferHomeExpertSwitch
(
prompt
:
string
):
boolean
{
const
normalized
=
prompt
.
normalize
(
"NFKC"
).
toLowerCase
();
const
normalized
=
prompt
.
normalize
(
"NFKC"
).
toLowerCase
();
...
@@ -520,32 +544,38 @@ function NavIcon({ kind }: { kind: "chat" | "experts" | "plugins" | "settings" |
...
@@ -520,32 +544,38 @@ function NavIcon({ kind }: { kind: "chat" | "experts" | "plugins" | "settings" |
case
"chat"
:
case
"chat"
:
return
(
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M6 6.75A2.75 2.75 0 0 1 8.75 4h6.5A2.75 2.75 0 0 1 18 6.75v4.5A2.75 2.75 0 0 1 15.25 14H12l-3.8 3.15c-.49.41-1.2.06-1.2-.58V14.9A2.75 2.75 0 0 1 6 12.25v-5.5Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.8"
/>
<
path
d=
"M5.25 7.1A2.85 2.85 0 0 1 8.1 4.25h6.2a2.85 2.85 0 0 1 2.85 2.85v3.8a2.85 2.85 0 0 1-2.85 2.85h-2.15l-3.46 2.82c-.5.41-1.25.05-1.25-.6v-2.27A2.85 2.85 0 0 1 5.25 10.9V7.1Z"
fill=
"#CCFBF1"
stroke=
"#0F766E"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.45"
/>
<
path
d=
"M11.55 9.15h5.2A2.25 2.25 0 0 1 19 11.4v2.75a2.25 2.25 0 0 1-2.25 2.25h-.9v1.32c0 .45-.52.7-.87.42L12.8 16.4h-1.25a2.25 2.25 0 0 1-2.25-2.25V11.4a2.25 2.25 0 0 1 2.25-2.25Z"
fill=
"#DBEAFE"
stroke=
"#2563EB"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.35"
/>
</
svg
>
</
svg
>
);
);
case
"experts"
:
case
"experts"
:
return
(
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"m12 3.75 1.9 3.85 4.25.62-3.08 3 .73 4.23L12 13.52 8.2 15.45l.73-4.23-3.08-3 4.25-.62L12 3.75Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.8"
/>
<
path
d=
"m12 3.75 1.9 3.85 4.25.62-3.08 3 .73 4.23L12 13.52 8.2 15.45l.73-4.23-3.08-3 4.25-.62L12 3.75Z"
fill=
"#EEF2FF"
stroke=
"#6366F1"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.55"
/>
<
path
d=
"m12 7.1.78 1.58 1.75.25-1.27 1.24.3 1.74L12 11.09l-1.56.82.3-1.74-1.27-1.24 1.75-.25L12 7.1Z"
fill=
"#F59E0B"
/>
</
svg
>
</
svg
>
);
);
case
"plugins"
:
case
"plugins"
:
return
(
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M9.25 4.75h2.5v3h3V4.5a1.75 1.75 0 1 1 3.5 0v3.4a2.1 2.1 0 0 1-2.1 2.1h-2.4v2.25H16a2 2 0 0 1 2 2V17a2.25 2.25 0 0 1-2.25 2.25H13.5v-2.5h-3v2.5H8.25A2.25 2.25 0 0 1 6 17v-2.75a2 2 0 0 1 2-2h2.25V10H7.9a2.1 2.1 0 0 1-2.1-2.1V4.5a1.75 1.75 0 1 1 3.5 0v3.25h3v-3Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.7"
/>
<
path
d=
"M9.1 4.5h3.15v3.15h2.45V5.42a1.67 1.67 0 1 1 3.35 0V8a2 2 0 0 1-2 2H12.2v2.2H9.1V9.95H6.95A1.95 1.95 0 0 1 5 8V5.42a1.67 1.67 0 1 1 3.35 0V7.65h.75V4.5Z"
fill=
"#E0E7FF"
stroke=
"#6366F1"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.35"
/>
<
path
d=
"M11.8 12.2h3.1v2.25h.75v-2.22a1.67 1.67 0 1 1 3.35 0v2.57a1.95 1.95 0 0 1-1.95 1.95H14.9v2.75h-3.15v-3.15H9.3v2.23a1.67 1.67 0 1 1-3.35 0V16a2 2 0 0 1 2-2h3.85v-1.8Z"
fill=
"#F3E8FF"
stroke=
"#7C3AED"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.35"
/>
</
svg
>
</
svg
>
);
);
case
"knowledge"
:
case
"knowledge"
:
return
(
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
<
path
d=
"M6 4.5A2.5 2.5 0 0 1 8.5 2h7.1L20 6.4v11.1a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 6 17.5v-13Z"
fill=
"#DBEAFE"
stroke=
"#2563EB"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.45"
/>
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.8"
/>
<
path
d=
"M15.45 2.2v3.1c0 .82.66 1.48 1.48 1.48h2.92"
fill=
"#BFDBFE"
stroke=
"#60A5FA"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.35"
/>
<
path
d=
"M9.1 10.2h5.8M9.1 13.15h4.25"
fill=
"none"
stroke=
"#1D4ED8"
strokeLinecap=
"round"
strokeWidth=
"1.35"
/>
<
path
d=
"m16.7 12.55.38.8.82.12-.6.58.15.82-.75-.4-.75.4.15-.82-.6-.58.82-.12.38-.8Z"
fill=
"#F59E0B"
/>
</
svg
>
</
svg
>
);
);
case
"settings"
:
case
"settings"
:
return
(
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M12 8.5A3.5 3.5 0 1 1 12 15.5 3.5 3.5 0 0 1 12 8.5Zm7 3.5-.88-.32a1.6 1.6 0 0 1-.97-2.08l.34-.86-1.7-1.7-.86.34a1.6 1.6 0 0 1-2.08-.97L12.5 5h-1l-.32.88a1.6 1.6 0 0 1-2.08.97l-.86-.34-1.7 1.7.34.86a1.6 1.6 0 0 1-.97 2.08L5 12v1l.88.32a1.6 1.6 0 0 1 .97 2.08l-.34.86 1.7 1.7.86-.34a1.6 1.6 0 0 1 2.08.97l.32.88h1l.32-.88a1.6 1.6 0 0 1 2.08-.97l.86.34 1.7-1.7-.34-.86a1.6 1.6 0 0 1 .97-2.08L19 13v-1Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.7"
/>
<
path
d=
"M12 8.35A3.65 3.65 0 1 1 12 15.65 3.65 3.65 0 0 1 12 8.35Zm7.2 3.38-.9-.32a1.55 1.55 0 0 1-.94-2.02l.34-.88-1.78-1.78-.88.35a1.55 1.55 0 0 1-2.02-.95L12.68 5h-1.36l-.34 1.13a1.55 1.55 0 0 1-2.02.95l-.88-.35L6.3 8.51l.34.88a1.55 1.55 0 0 1-.94 2.02l-.9.32v1.54l.9.32a1.55 1.55 0 0 1 .94 2.02l-.34.88 1.78 1.78.88-.35a1.55 1.55 0 0 1 2.02.95l.34 1.13h1.36l.34-1.13a1.55 1.55 0 0 1 2.02-.95l.88.35 1.78-1.78-.34-.88a1.55 1.55 0 0 1 .94-2.02l.9-.32v-1.54Z"
fill=
"#CFFAFE"
stroke=
"#0891B2"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.35"
/>
<
circle
cx=
"12"
cy=
"12"
r=
"1.85"
fill=
"#2563EB"
/>
</
svg
>
</
svg
>
);
);
}
}
...
@@ -674,34 +704,70 @@ const ui = {
...
@@ -674,34 +704,70 @@ const ui = {
const
CATEGORY_CONFIG
=
[
const
CATEGORY_CONFIG
=
[
{
{
id
:
'content'
,
id
:
'content'
,
name
:
'内容营销'
,
name
:
'内容营销'
icon
:
'📝'
,
color
:
'rgba(109, 93, 252, 0.2)'
,
hoverColor
:
'rgba(109, 93, 252, 0.3)'
},
},
{
{
id
:
'acquisition'
,
id
:
'acquisition'
,
name
:
'精准获客'
,
name
:
'精准获客'
icon
:
'🎯'
,
color
:
'rgba(141, 156, 255, 0.2)'
,
hoverColor
:
'rgba(141, 156, 255, 0.3)'
},
},
{
{
id
:
'sales'
,
id
:
'sales'
,
name
:
'销售冠军'
,
name
:
'销售冠军'
icon
:
'🏆'
,
color
:
'rgba(86, 205, 255, 0.2)'
,
hoverColor
:
'rgba(86, 205, 255, 0.3)'
},
},
{
{
id
:
'other'
,
id
:
'other'
,
name
:
'其他专家'
,
name
:
'其他专家'
icon
:
'📁'
,
color
:
'rgba(168, 157, 255, 0.2)'
,
hoverColor
:
'rgba(168, 157, 255, 0.3)'
}
}
]
as
const
;
]
as
const
;
type
ExpertCategoryId
=
typeof
CATEGORY_CONFIG
[
number
][
"id"
];
function
ExpertCategoryIcon
({
kind
}:
{
kind
:
ExpertCategoryId
})
{
switch
(
kind
)
{
case
"content"
:
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M5.4 5.4A2.4 2.4 0 0 1 7.8 3h7.05L19 7.15v10.45a2.4 2.4 0 0 1-2.4 2.4H7.8a2.4 2.4 0 0 1-2.4-2.4V5.4Z"
fill=
"#F5F3FF"
stroke=
"#7C3AED"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.35"
/>
<
path
d=
"M14.7 3.2v2.75c0 .78.63 1.41 1.41 1.41h2.7"
fill=
"#EDE9FE"
stroke=
"#A78BFA"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.25"
/>
<
path
d=
"M8.5 10.05h6.35M8.5 13h4.55"
fill=
"none"
stroke=
"#6D5DFC"
strokeLinecap=
"round"
strokeWidth=
"1.35"
/>
<
circle
cx=
"15.85"
cy=
"14.85"
r=
"1.55"
fill=
"#F97316"
/>
<
path
d=
"M15.85 13.95v1.8M14.95 14.85h1.8"
stroke=
"#FFF7ED"
strokeLinecap=
"round"
strokeWidth=
"1"
/>
</
svg
>
);
case
"acquisition"
:
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
circle
cx=
"12"
cy=
"12"
r=
"7.3"
fill=
"#ECFEFF"
stroke=
"#06B6D4"
strokeWidth=
"1.35"
/>
<
circle
cx=
"12"
cy=
"12"
r=
"4.45"
fill=
"#DBEAFE"
stroke=
"#2563EB"
strokeWidth=
"1.25"
/>
<
circle
cx=
"12"
cy=
"12"
r=
"1.72"
fill=
"#22C55E"
stroke=
"#F0FDF4"
strokeWidth=
"0.8"
/>
<
path
d=
"M12 3.7v2.2M20.3 12h-2.2M12 20.3v-2.2M3.7 12h2.2"
fill=
"none"
stroke=
"#0F766E"
strokeLinecap=
"round"
strokeWidth=
"1.25"
/>
<
path
d=
"m17.3 5.35 1.25-.8-.35 1.45 1.4.48-1.42.43.3 1.47-1.2-.86-1.1.98.2-1.46-1.45-.32 1.36-.57-.47-1.4 1.25.78.23-.18Z"
fill=
"#F59E0B"
/>
</
svg
>
);
case
"sales"
:
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M8 5.15h8v2.2a4 4 0 0 1-3 3.88v1.92h2.15a1.35 1.35 0 0 1 1.35 1.35V16H7.5v-1.5a1.35 1.35 0 0 1 1.35-1.35H11v-1.92a4 4 0 0 1-3-3.88v-2.2Z"
fill=
"#FEF3C7"
stroke=
"#D97706"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.35"
/>
<
path
d=
"M8 6.05H5.85A1.6 1.6 0 0 0 4.25 7.65c0 1.72 1.39 3.1 3.1 3.1H8m8-4.7h2.15a1.6 1.6 0 0 1 1.6 1.6c0 1.72-1.39 3.1-3.1 3.1H16"
fill=
"#FFFBEB"
stroke=
"#F59E0B"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.25"
/>
<
path
d=
"M8.75 16h6.5v3.1h-6.5z"
fill=
"#EDE9FE"
stroke=
"#7C3AED"
strokeLinejoin=
"round"
strokeWidth=
"1.2"
/>
<
path
d=
"M9.75 20h4.5"
stroke=
"#7C3AED"
strokeLinecap=
"round"
strokeWidth=
"1.35"
/>
<
path
d=
"m12 7.15.58 1.16 1.28.19-.93.9.22 1.27L12 10.07l-1.15.6.22-1.27-.93-.9 1.28-.19L12 7.15Z"
fill=
"#EF4444"
/>
</
svg
>
);
case
"other"
:
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M4.2 7.7A2.2 2.2 0 0 1 6.4 5.5h11.2a2.2 2.2 0 0 1 2.2 2.2v8.1a2.2 2.2 0 0 1-2.2 2.2H6.4a2.2 2.2 0 0 1-2.2-2.2V7.7Z"
fill=
"#EEF2FF"
stroke=
"#4F46E5"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.35"
/>
<
path
d=
"M4.85 9.05h14.3"
stroke=
"#60A5FA"
strokeLinecap=
"round"
strokeWidth=
"1.2"
/>
<
circle
cx=
"7.05"
cy=
"7.35"
r=
"0.72"
fill=
"#22C55E"
/>
<
circle
cx=
"9.45"
cy=
"7.35"
r=
"0.72"
fill=
"#F59E0B"
/>
<
path
d=
"M9 12.35h6M9 15h3.7"
stroke=
"#7C3AED"
strokeLinecap=
"round"
strokeWidth=
"1.25"
/>
<
path
d=
"m16.15 12.1.3.62.68.1-.49.47.12.67-.61-.32-.6.32.11-.67-.49-.47.68-.1.3-.62Z"
fill=
"#EF4444"
/>
</
svg
>
);
}
}
const
startupCurtainCopy
=
{
const
startupCurtainCopy
=
{
brandTitle
:
"
\
u5343
\
u5320
\
u00b7
\
u95ee
\
u5929"
,
brandTitle
:
"
\
u5343
\
u5320
\
u00b7
\
u95ee
\
u5929"
,
brandTagline
:
"START YOUR IDEAS"
,
brandTagline
:
"START YOUR IDEAS"
,
...
@@ -2112,12 +2178,18 @@ export default function App() {
...
@@ -2112,12 +2178,18 @@ export default function App() {
const [sidebarSessionTitles, setSidebarSessionTitles] = useState<Record<string, string>>({});
const [sidebarSessionTitles, setSidebarSessionTitles] = useState<Record<string, string>>({});
const [skillMenuOpen, setSkillMenuOpen] = useState(false);
const [skillMenuOpen, setSkillMenuOpen] = useState(false);
const [isComposerDragOver, setIsComposerDragOver] = useState(false);
const [isComposerDragOver, setIsComposerDragOver] = useState(false);
const [isComposerResizeActive, setIsComposerResizeActive] = useState(false);
const [composerTextareaRatio, setComposerTextareaRatio] = useState(COMPOSER_TEXTAREA_DEFAULT_RATIO);
const [composerTextareaHeight, setComposerTextareaHeight] = useState(96);
const [composerWorkspaceHeight, setComposerWorkspaceHeight] = useState(0);
const [copiedToken, setCopiedToken] = useState("");
const [copiedToken, setCopiedToken] = useState("");
const activeStreamRef = useRef<ActiveStreamState | null>(null);
const activeStreamRef = useRef<ActiveStreamState | null>(null);
const skillMenuRef = useRef<HTMLDivElement | null>(null);
const skillMenuRef = useRef<HTMLDivElement | null>(null);
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
const conversationWorkspaceRef = useRef<HTMLDivElement | null>(null);
const copiedTokenResetRef = useRef<number | null>(null);
const copiedTokenResetRef = useRef<number | null>(null);
const composerDragDepthRef = useRef(0);
const composerDragDepthRef = useRef(0);
const composerResizeDragRef = useRef<{ startY: number; startHeight: number; workspaceHeight: number } | null>(null);
const startupWarmupRequestedRef = useRef(false);
const startupWarmupRequestedRef = useRef(false);
const lastLoadedWorkspacePathRef = useRef<string | null>(null);
const lastLoadedWorkspacePathRef = useRef<string | null>(null);
const [streamSmoke, setStreamSmoke] = useState<SmokeStreamSnapshot | null>(null);
const [streamSmoke, setStreamSmoke] = useState<SmokeStreamSnapshot | null>(null);
...
@@ -2264,6 +2336,38 @@ export default function App() {
...
@@ -2264,6 +2336,38 @@ export default function App() {
const isConversationView = viewMode === "chat" || viewMode === "experts";
const isConversationView = viewMode === "chat" || viewMode === "experts";
const showInlineStartupNotice = startupStateActive && hasVisibleConversation && isConversationView;
const showInlineStartupNotice = startupStateActive && hasVisibleConversation && isConversationView;
const pluginGroups = useMemo(() => groupPluginsByStatus(workspace?.plugins), [workspace?.plugins]);
const pluginGroups = useMemo(() => groupPluginsByStatus(workspace?.plugins), [workspace?.plugins]);
const composerTextareaBounds = useMemo(() => getComposerTextareaBounds(composerWorkspaceHeight), [composerWorkspaceHeight]);
useEffect(() => {
if (!isConversationView) {
return;
}
const workspaceElement = conversationWorkspaceRef.current;
if (!workspaceElement) {
return;
}
const updateComposerHeight = (workspaceHeight: number) => {
const safeWorkspaceHeight = Number.isFinite(workspaceHeight) && workspaceHeight > 0 ? workspaceHeight : 0;
setComposerWorkspaceHeight((currentHeight) => Math.abs(currentHeight - safeWorkspaceHeight) < 0.5 ? currentHeight : safeWorkspaceHeight);
const nextHeight = clampComposerTextareaHeight(safeWorkspaceHeight * composerTextareaRatio, safeWorkspaceHeight);
setComposerTextareaHeight((currentHeight) => Math.abs(currentHeight - nextHeight) < 0.5 ? currentHeight : nextHeight);
};
updateComposerHeight(workspaceElement.getBoundingClientRect().height);
if (!("ResizeObserver" in window)) {
return;
}
const observer = new ResizeObserver((entries) => {
updateComposerHeight(entries[0]?.contentRect.height ?? workspaceElement.getBoundingClientRect().height);
});
observer.observe(workspaceElement);
return () => observer.disconnect();
}, [composerTextareaRatio, isConversationView]);
useEffect(() => {
useEffect(() => {
if (viewMode !== "chat" && viewMode !== "experts") {
if (viewMode !== "chat" && viewMode !== "experts") {
...
@@ -4150,6 +4254,50 @@ export default function App() {
...
@@ -4150,6 +4254,50 @@ export default function App() {
}
}
}
}
function handleComposerResizePointerDown(event: ReactPointerEvent<HTMLDivElement>) {
if (event.button !== 0) {
return;
}
const workspaceHeight = conversationWorkspaceRef.current?.getBoundingClientRect().height ?? composerWorkspaceHeight;
setComposerWorkspaceHeight((currentHeight) => Math.abs(currentHeight - workspaceHeight) < 0.5 ? currentHeight : workspaceHeight);
composerResizeDragRef.current = {
startY: event.clientY,
startHeight: composerTextareaHeight,
workspaceHeight
};
event.currentTarget.setPointerCapture(event.pointerId);
setIsComposerResizeActive(true);
event.preventDefault();
}
function handleComposerResizePointerMove(event: ReactPointerEvent<HTMLDivElement>) {
const resizeState = composerResizeDragRef.current;
if (!resizeState) {
return;
}
const workspaceHeight = conversationWorkspaceRef.current?.getBoundingClientRect().height || resizeState.workspaceHeight;
const nextHeight = clampComposerTextareaHeight(resizeState.startHeight + resizeState.startY - event.clientY, workspaceHeight);
setComposerWorkspaceHeight((currentHeight) => Math.abs(currentHeight - workspaceHeight) < 0.5 ? currentHeight : workspaceHeight);
setComposerTextareaHeight(nextHeight);
setComposerTextareaRatio(nextHeight / Math.max(workspaceHeight, 1));
event.preventDefault();
}
function handleComposerResizePointerEnd(event: ReactPointerEvent<HTMLDivElement>) {
if (!composerResizeDragRef.current) {
return;
}
composerResizeDragRef.current = null;
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
setIsComposerResizeActive(false);
event.preventDefault();
}
async function openAttachmentPicker() {
async function openAttachmentPicker() {
if (window.qjcDesktop) {
if (window.qjcDesktop) {
const attachments = await desktopApi.chat.pickAttachments();
const attachments = await desktopApi.chat.pickAttachments();
...
@@ -4407,6 +4555,11 @@ export default function App() {
...
@@ -4407,6 +4555,11 @@ export default function App() {
</button>
</button>
);
);
const conversationPanelTitle = viewMode === "experts" ? activeExpertName : "对话";
const conversationPanelTitle = viewMode === "experts" ? activeExpertName : "对话";
const expertWorkspaceLogo = viewMode === "experts" && (activeExpertKey === "xiaohongshu" || activeExpertKey === "douyin") ? (
<span className={"expert-workspace-logo expert-workspace-logo-" + activeExpertKey} aria-hidden="true">
{activeExpertKey === "xiaohongshu" ? <RedBookIcon /> : <DouyinNoteIcon />}
</span>
) : null;
const conversationPanelLead = viewMode === "chat" ? (
const conversationPanelLead = viewMode === "chat" ? (
<div className="home-microcopy" aria-label="start your idea">
<div className="home-microcopy" aria-label="start your idea">
<span className="home-microcopy-icon">
<span className="home-microcopy-icon">
...
@@ -4417,9 +4570,11 @@ export default function App() {
...
@@ -4417,9 +4570,11 @@ export default function App() {
</div>
</div>
) : (
) : (
<div className="conversation-panel-kicker expert-hero-kicker">
<div className="conversation-panel-kicker expert-hero-kicker">
<span className={"expert-hero-icon expert-hero-icon-" + activeExpertVisualKey} aria-hidden="true">
{expertWorkspaceLogo ?? (
{renderExpertIcon(activeExpertVisualKey)}
<span className={"expert-hero-icon expert-hero-icon-" + activeExpertVisualKey} aria-hidden="true">
</span>
{renderExpertIcon(activeExpertVisualKey)}
</span>
)}
<span className="expert-hero-copy">
<span className="expert-hero-copy">
{/* <span className="expert-hero-label">当前专家</span> */}
{/* <span className="expert-hero-label">当前专家</span> */}
<strong>{conversationPanelTitle}</strong>
<strong>{conversationPanelTitle}</strong>
...
@@ -4689,15 +4844,22 @@ export default function App() {
...
@@ -4689,15 +4844,22 @@ export default function App() {
: viewMode === "experts" && !expertPageProjects.length
: viewMode === "experts" && !expertPageProjects.length
? <div className="empty-state">{expertsPageCopy.noExperts}</div>
? <div className="empty-state">{expertsPageCopy.noExperts}</div>
: messageListContent;
: messageListContent;
const composerShellStyle = {
"--composer-textarea-height": `
$
{
Math
.
round
(
composerTextareaHeight
)}
px
`,
"--composer-textarea-min-height": `
$
{
Math
.
round
(
composerTextareaBounds
.
min
)}
px
`,
"--composer-textarea-max-height": `
$
{
Math
.
round
(
composerTextareaBounds
.
max
)}
px
`
} as CSSProperties;
const composerContent = (
const composerContent = (
<form
<form
className={
className={
"composer-shell"
"composer-shell"
+ (isComposerDragOver ? " dragging" : "")
+ (isComposerDragOver ? " dragging" : "")
+ (isComposerResizeActive ? " resizing" : "")
+ (viewMode === "chat" ? " composer-shell-home" : "")
+ (viewMode === "chat" ? " composer-shell-home" : "")
+ (viewMode === "experts" ? " composer-shell-expert" : "")
+ (viewMode === "experts" ? " composer-shell-expert" : "")
}
}
style={composerShellStyle}
onSubmit={(event) => {
onSubmit={(event) => {
event.preventDefault();
event.preventDefault();
void sendPrompt();
void sendPrompt();
...
@@ -4717,6 +4879,19 @@ export default function App() {
...
@@ -4717,6 +4879,19 @@ export default function App() {
onChange={handleAttachmentSelection}
onChange={handleAttachmentSelection}
/>
/>
{isComposerDragOver ? <div className="composer-drop-indicator">释放以上传附件</div> : null}
{isComposerDragOver ? <div className="composer-drop-indicator">释放以上传附件</div> : null}
<div
className="composer-resize-handle"
role="separator"
aria-orientation="horizontal"
aria-label={"\u8c03\u6574\u8f93\u5165\u6846\u9ad8\u5ea6"}
title={"\u8c03\u6574\u8f93\u5165\u6846\u9ad8\u5ea6"}
onPointerDown={handleComposerResizePointerDown}
onPointerMove={handleComposerResizePointerMove}
onPointerUp={handleComposerResizePointerEnd}
onPointerCancel={handleComposerResizePointerEnd}
>
<span aria-hidden="true" />
</div>
<div className="composer-surface">
<div className="composer-surface">
<label className="composer-field">
<label className="composer-field">
<textarea
<textarea
...
@@ -4803,20 +4978,11 @@ export default function App() {
...
@@ -4803,20 +4978,11 @@ export default function App() {
<WindowControlIcon kind="close" />
<WindowControlIcon kind="close" />
</button>
</button>
</div>
</div>
<aside className=
{"sidebar app-drag-region" + (isConversationView ? " conversation-sidebar-layout" : "")}
>
<aside className=
"sidebar conversation-sidebar-layout app-drag-region"
>
<div className="sidebar-top">
<div className="sidebar-top">
<div className="sidebar-logo-block" aria-label="千匠问天">
<div className="sidebar-logo-mark-shell" aria-hidden="true">
<img src={brandIcon} alt="" className="sidebar-logo-mark" />
</div>
<div className="sidebar-logo-copy">
<strong>千匠问天</strong>
</div>
</div>
<nav className="nav-list">
<nav className="nav-list">
{[
{[
{ id: "chat" as const, label: "对话" },
{ id: "chat" as const, label: "对话" },
{ id: "experts" as const, label: "数字员工" },
{ id: "knowledge" as const, label: ui.knowledge },
{ id: "knowledge" as const, label: ui.knowledge },
{ id: "plugins" as const, label: ui.plugins },
{ id: "plugins" as const, label: ui.plugins },
{ id: "settings" as const, label: ui.settings }
{ id: "settings" as const, label: ui.settings }
...
@@ -4833,6 +4999,11 @@ export default function App() {
...
@@ -4833,6 +4999,11 @@ export default function App() {
</div>
</div>
<div className="sidebar-bottom">
<div className="sidebar-bottom">
<section className="sidebar-section compact sidebar-experts-entry">
<section className="sidebar-section compact sidebar-experts-entry">
<div className="sidebar-section-head sidebar-digital-workers-title">
<div className="sidebar-section-copy">
<strong className="sidebar-section-title">数字员工</strong>
</div>
</div>
<div className="sidebar-expert-scroll">
<div className="sidebar-expert-scroll">
<div className="expert-category-list">
<div className="expert-category-list">
{CATEGORY_CONFIG.map((category) => {
{CATEGORY_CONFIG.map((category) => {
...
@@ -4864,18 +5035,25 @@ export default function App() {
...
@@ -4864,18 +5035,25 @@ export default function App() {
});
});
const isExpanded = expandedCategories[category.id] || false;
const isExpanded = expandedCategories[category.id] || false;
const categoryPanelId = `
expert
-
tree
-
$
{
category
.
id
}
`;
const hasExperts = categoryExperts.length > 0;
return (
return (
<div
<div
key={category.id}
key={category.id}
className="expert-category-item"
className="expert-category-item
expert-tree-category
"
>
>
{/* 分类头部 - 可点击展开/收起 */}
<button
<div
type="button"
className="expert-category-header"
className="expert-category-header
expert-tree-category-trigger app-no-drag
"
onClick={() => toggleCategory(category.id)}
onClick={() => toggleCategory(category.id)}
disabled={!hasExperts}
aria-expanded={isExpanded}
aria-controls={categoryPanelId}
>
>
<div className="expert-category-icon">{category.icon}</div>
<span className="expert-category-icon" aria-hidden="true">
<ExpertCategoryIcon kind={category.id} />
</span>
<div className="expert-category-title">
<div className="expert-category-title">
<div className="expert-category-name">{category.name}</div>
<div className="expert-category-name">{category.name}</div>
</div>
</div>
...
@@ -4884,11 +5062,11 @@ export default function App() {
...
@@ -4884,11 +5062,11 @@ export default function App() {
<path d="M6 9l6 6 6-6"/>
<path d="M6 9l6 6 6-6"/>
</svg>
</svg>
</div>
</div>
</
div
>
</
button
>
{/* 展开的内容区域 */}
{/* 展开的内容区域 */}
{isExpanded &&
categoryExperts.length > 0
&& (
{isExpanded &&
hasExperts
&& (
<div
className="expert-category-conten
t">
<div
id={categoryPanelId} className="expert-category-content expert-tree-lis
t">
<div className="expert-category-experts">
<div className="expert-category-experts">
{categoryExperts.map((entry) => {
{categoryExperts.map((entry) => {
const expertVisualKey = resolveExpertVisualKey(entry.project, entry.definition);
const expertVisualKey = resolveExpertVisualKey(entry.project, entry.definition);
...
@@ -4898,16 +5076,17 @@ export default function App() {
...
@@ -4898,16 +5076,17 @@ export default function App() {
: viewMode === "chat" && prompt.trim() === buildShortcutPrompt(entry.definition);
: viewMode === "chat" && prompt.trim() === buildShortcutPrompt(entry.definition);
return (
return (
<div
<button
type="button"
key={entry.definition.id}
key={entry.definition.id}
className=
"expert-category-expert-item"
className=
{"expert-category-expert-item expert-tree-expert app-no-drag" + (isActive ? " active" : "")}
onClick={() => handleExpertSelect(entry)}
onClick={() => handleExpertSelect(entry)}
>
>
<div className="expert-category-expert-icon">
<div className="expert-category-expert-icon">
{renderExpertIcon(expertVisualKey)}
{renderExpertIcon(expertVisualKey)}
</div>
</div>
<div className="expert-category-expert-name">{entry.displayName}</div>
<div className="expert-category-expert-name">{entry.displayName}</div>
</
div
>
</
button
>
);
);
})}
})}
</div>
</div>
...
@@ -4980,7 +5159,7 @@ export default function App() {
...
@@ -4980,7 +5159,7 @@ export default function App() {
{isMockDesktopApi ? <StatusChip tone="warning">Mock API</StatusChip> : null}
{isMockDesktopApi ? <StatusChip tone="warning">Mock API</StatusChip> : null}
</div>
</div>
</div>
</div>
<div className="conversation-workspace">
<div className="conversation-workspace"
ref={conversationWorkspaceRef}
>
{conversationStatusNotice}
{conversationStatusNotice}
{viewMode === "chat" ? homeIntentSuggestionNotice : null}
{viewMode === "chat" ? homeIntentSuggestionNotice : null}
<div className="conversation-panel-body">
<div className="conversation-panel-body">
...
...
apps/ui/src/styles.css
View file @
f4e11f94
...
@@ -280,8 +280,10 @@ strong { font-weight: 600; }
...
@@ -280,8 +280,10 @@ strong { font-weight: 600; }
color
:
var
(
--color-text-secondary
);
color
:
var
(
--color-text-secondary
);
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
8px
;
font-family
:
"Microsoft YaHei UI"
,
"PingFang SC"
,
"Segoe UI"
,
sans-serif
;
font-size
:
14px
;
font-size
:
14px
;
font-weight
:
5
00
;
font-weight
:
6
00
;
box-shadow
:
none
;
box-shadow
:
none
;
transition
:
all
0.2s
ease
;
transition
:
all
0.2s
ease
;
}
}
...
@@ -2810,7 +2812,8 @@ button.secondary {
...
@@ -2810,7 +2812,8 @@ button.secondary {
}
}
.sidebar-section-title
{
.sidebar-section-title
{
font-size
:
15px
;
font-size
:
14px
;
font-weight
:
600
;
}
}
.sidebar-experts-entry
{
.sidebar-experts-entry
{
...
@@ -2834,20 +2837,19 @@ button.secondary {
...
@@ -2834,20 +2837,19 @@ button.secondary {
.expert-category-list
{
.expert-category-list
{
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
gap
:
4
px
;
gap
:
6
px
;
}
}
.expert-category-item
{
.expert-category-item
{
border-radius
:
1
6
px
;
border-radius
:
1
4
px
;
background
:
linear-gradient
(
135deg
,
background
:
linear-gradient
(
135deg
,
rgba
(
235
,
240
,
255
,
0.9
),
rgba
(
235
,
240
,
255
,
0.9
),
rgba
(
245
,
240
,
255
,
0.9
)
rgba
(
245
,
240
,
255
,
0.9
)
);
);
border
:
1px
solid
rgba
(
109
,
93
,
252
,
0.15
);
border
:
1px
solid
rgba
(
109
,
93
,
252
,
0.15
);
box-shadow
:
0
4px
12px
rgba
(
109
,
93
,
252
,
0.08
);
box-shadow
:
0
4px
12px
rgba
(
109
,
93
,
252
,
0.08
);
transition
:
all
0.2s
ease
;
overflow
:
hidden
;
position
:
relative
;
transition
:
border-color
180ms
ease
,
box-shadow
180ms
ease
,
background
180ms
ease
;
margin-bottom
:
4px
;
}
}
.expert-category-item
:hover
{
.expert-category-item
:hover
{
...
@@ -2856,13 +2858,21 @@ button.secondary {
...
@@ -2856,13 +2858,21 @@ button.secondary {
}
}
.expert-category-header
{
.expert-category-header
{
width
:
100%
;
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
2px
;
gap
:
8px
;
padding
:
6px
16px
;
min-height
:
40px
;
padding
:
0
10px
;
border
:
0
;
border-radius
:
0
;
background
:
transparent
;
color
:
inherit
;
cursor
:
pointer
;
cursor
:
pointer
;
box-shadow
:
none
;
user-select
:
none
;
user-select
:
none
;
min-height
:
40px
;
/* 进一步缩小高度 */
text-align
:
left
;
transition
:
background
180ms
ease
,
color
180ms
ease
;
}
}
.expert-category-header
:hover
{
.expert-category-header
:hover
{
...
@@ -2870,39 +2880,53 @@ button.secondary {
...
@@ -2870,39 +2880,53 @@ button.secondary {
rgba
(
235
,
240
,
255
,
0.95
),
rgba
(
235
,
240
,
255
,
0.95
),
rgba
(
245
,
240
,
255
,
0.95
)
rgba
(
245
,
240
,
255
,
0.95
)
);
);
box-shadow
:
none
;
transform
:
none
;
}
.expert-category-header
:disabled
{
cursor
:
default
;
opacity
:
0.58
;
}
.expert-category-header
:disabled:hover
{
background
:
transparent
;
}
}
.expert-category-icon
{
.expert-category-icon
{
font-size
:
14px
;
width
:
22px
;
line-height
:
1
;
height
:
22px
;
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
flex-shrink
:
0
;
color
:
#6D5DFC
;
}
.expert-category-icon
svg
{
width
:
18px
;
height
:
18px
;
}
}
.expert-category-title
{
.expert-category-title
{
flex
:
1
;
flex
:
0
1
auto
;
min-width
:
0
;
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
gap
:
4px
;
gap
:
4px
;
}
}
.expert-category-name
{
.expert-category-name
{
font-size
:
12px
;
font-family
:
"Microsoft YaHei UI"
,
"PingFang SC"
,
"Segoe UI"
,
sans-serif
;
font-size
:
14px
;
font-weight
:
600
;
font-weight
:
600
;
color
:
#4A5568
;
color
:
#4A5568
;
text-align
:
left
;
line-height
:
1.2
;
line-height
:
1.2
;
}
}
.expert-category-count
{
font-size
:
10px
;
font-weight
:
500
;
color
:
#6D5DFC
;
background
:
rgba
(
109
,
93
,
252
,
0.1
);
padding
:
2px
8px
;
border-radius
:
10px
;
align-self
:
flex-start
;
}
.expert-category-toggle
{
.expert-category-toggle
{
margin-left
:
auto
;
width
:
20px
;
width
:
20px
;
height
:
20px
;
height
:
20px
;
display
:
flex
;
display
:
flex
;
...
@@ -2918,83 +2942,51 @@ button.secondary {
...
@@ -2918,83 +2942,51 @@ button.secondary {
}
}
.expert-category-content
{
.expert-category-content
{
position
:
absolute
;
position
:
static
;
top
:
100%
;
left
:
0
;
right
:
0
;
z-index
:
1000
;
background
:
linear-gradient
(
135deg
,
background
:
linear-gradient
(
135deg
,
rgba
(
255
,
255
,
255
,
0.
98
),
rgba
(
255
,
255
,
255
,
0.
62
),
rgba
(
245
,
240
,
255
,
0.
96
)
rgba
(
245
,
240
,
255
,
0.
58
)
);
);
border
:
1px
solid
rgba
(
109
,
93
,
252
,
0.2
);
border-top
:
1px
solid
rgba
(
109
,
93
,
252
,
0.12
);
border-radius
:
16px
;
box-shadow
:
none
;
box-shadow
:
margin-top
:
0
;
0
22px
48px
rgba
(
109
,
93
,
252
,
0.12
),
0
8px
20px
rgba
(
109
,
93
,
252
,
0.08
);
margin-top
:
8px
;
animation
:
slide-down
0.2s
ease-out
;
overflow
:
hidden
;
}
@keyframes
slide-down
{
0
%
{
opacity
:
0
;
transform
:
translateY
(
-8px
);
}
100
%
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
}
}
.expert-category-experts
{
.expert-category-experts
{
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
gap
:
8px
;
gap
:
4px
;
padding
:
16px
;
padding
:
6px
;
max-height
:
320px
;
overflow-y
:
auto
;
/* 透明滚动条 */
scrollbar-width
:
thin
;
scrollbar-color
:
rgba
(
139
,
92
,
246
,
0.3
)
transparent
;
}
.expert-category-experts
::-webkit-scrollbar
{
width
:
4px
;
}
.expert-category-experts
::-webkit-scrollbar-track
{
background
:
transparent
;
border-radius
:
2px
;
}
.expert-category-experts
::-webkit-scrollbar-thumb
{
background
:
rgba
(
139
,
92
,
246
,
0.3
);
border-radius
:
2px
;
transition
:
background
0.2s
ease
;
}
.expert-category-experts
::-webkit-scrollbar-thumb:hover
{
background
:
rgba
(
139
,
92
,
246
,
0.5
);
}
}
.expert-category-expert-item
{
.expert-category-expert-item
{
width
:
100%
;
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
10
px
;
gap
:
8
px
;
padding
:
10px
12
px
;
min-height
:
34
px
;
border-radius
:
12
px
;
padding
:
6px
8px
6px
28
px
;
b
ackground
:
rgba
(
255
,
255
,
255
,
0.7
)
;
b
order-radius
:
10px
;
border
:
1px
solid
rgba
(
109
,
93
,
252
,
0.1
);
border
:
1px
solid
rgba
(
109
,
93
,
252
,
0.1
);
background
:
rgba
(
255
,
255
,
255
,
0.7
);
color
:
#4A5568
;
cursor
:
pointer
;
cursor
:
pointer
;
transition
:
all
0.15s
ease
;
box-shadow
:
none
;
text-align
:
left
;
transition
:
background
150ms
ease
,
border-color
150ms
ease
,
color
150ms
ease
;
}
}
.expert-category-expert-item
:hover
{
.expert-category-expert-item
:hover
{
background
:
rgba
(
109
,
93
,
252
,
0.05
);
background
:
rgba
(
109
,
93
,
252
,
0.05
);
border-color
:
rgba
(
109
,
93
,
252
,
0.2
);
border-color
:
rgba
(
109
,
93
,
252
,
0.2
);
transform
:
translateY
(
-1px
);
box-shadow
:
none
;
transform
:
none
;
}
.expert-category-expert-item.active
{
color
:
#5b4fe8
;
background
:
rgba
(
109
,
93
,
252
,
0.1
);
border-color
:
rgba
(
109
,
93
,
252
,
0.24
);
}
}
.expert-category-expert-icon
{
.expert-category-expert-icon
{
...
@@ -3067,22 +3059,14 @@ button.secondary {
...
@@ -3067,22 +3059,14 @@ button.secondary {
}
}
.expert-category-name
{
.expert-category-name
{
font-size
:
12px
;
font-family
:
"Microsoft YaHei UI"
,
"PingFang SC"
,
"Segoe UI"
,
sans-serif
;
font-size
:
14px
;
font-weight
:
600
;
font-weight
:
600
;
color
:
#4A5568
;
color
:
#4A5568
;
text-align
:
center
;
text-align
:
left
;
line-height
:
1.2
;
line-height
:
1.2
;
}
}
.expert-category-count
{
font-size
:
10px
;
font-weight
:
500
;
color
:
#6D5DFC
;
background
:
rgba
(
109
,
93
,
252
,
0.1
);
padding
:
2px
6px
;
border-radius
:
8px
;
}
.expert-chip-list
{
.expert-chip-list
{
gap
:
10px
;
gap
:
10px
;
}
}
...
@@ -3134,12 +3118,6 @@ button.secondary {
...
@@ -3134,12 +3118,6 @@ button.secondary {
position
:
relative
;
position
:
relative
;
}
}
.sidebar-expert-scroll
,
.sidebar-section
,
.sidebar-bottom
{
overflow
:
visible
!important
;
}
@keyframes
popup-fade-in
{
@keyframes
popup-fade-in
{
0
%
{
0
%
{
opacity
:
0
;
opacity
:
0
;
...
@@ -4768,7 +4746,7 @@ button.secondary {
...
@@ -4768,7 +4746,7 @@ button.secondary {
}
}
.shell
{
.shell
{
grid-template-columns
:
280px
minmax
(
0
,
1
fr
);
grid-template-columns
:
clamp
(
232px
,
19vw
,
280px
)
minmax
(
0
,
1
fr
);
background
:
#f0f7ff
;
background
:
#f0f7ff
;
}
}
...
@@ -4776,13 +4754,17 @@ button.secondary {
...
@@ -4776,13 +4754,17 @@ button.secondary {
display
:
none
;
display
:
none
;
}
}
.conversation-shell
.conversation-sidebar-layout
{
.conversation-shell
{
grid-template-columns
:
clamp
(
232px
,
19vw
,
280px
)
minmax
(
0
,
1
fr
);
}
.conversation-sidebar-layout
{
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
gap
:
1
6
px
;
gap
:
1
2
px
;
}
}
.
conversation-shell
.
sidebar-top
{
.sidebar-top
{
gap
:
1
4
px
;
gap
:
1
0
px
;
}
}
.conversation-sidebar-action
{
.conversation-sidebar-action
{
...
@@ -4798,47 +4780,95 @@ button.secondary {
...
@@ -4798,47 +4780,95 @@ button.secondary {
border-radius
:
16px
;
border-radius
:
16px
;
}
}
.
conversation-shell
.
sidebar-bottom
{
.sidebar-bottom
{
min-height
:
0
;
min-height
:
0
;
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
overflow
:
hidden
;
overflow-y
:
auto
;
overflow-x
:
hidden
;
gap
:
12px
;
gap
:
12px
;
padding-right
:
0
;
padding-right
:
2px
;
scrollbar-width
:
thin
;
scrollbar-color
:
rgba
(
139
,
92
,
246
,
0.28
)
transparent
;
}
}
.conversation-shell
.sidebar-experts-entry
,
.sidebar-bottom
::-webkit-scrollbar
{
.conversation-shell
.sidebar-session-section
{
width
:
4px
;
flex
:
1
1
0
;
}
min-height
:
0
;
.sidebar-bottom
::-webkit-scrollbar-track
{
background
:
transparent
;
}
.sidebar-bottom
::-webkit-scrollbar-thumb
{
border-radius
:
999px
;
background
:
rgba
(
139
,
92
,
246
,
0.28
);
}
.sidebar-experts-entry
,
.sidebar-session-section
{
padding
:
14px
;
padding
:
14px
;
border-radius
:
22px
;
border-radius
:
22px
;
background
:
linear-gradient
(
180deg
,
rgba
(
255
,
255
,
255
,
0.98
),
rgba
(
247
,
251
,
250
,
0.96
));
background
:
linear-gradient
(
180deg
,
rgba
(
255
,
255
,
255
,
0.98
),
rgba
(
247
,
251
,
250
,
0.96
));
box-shadow
:
0
18px
34px
rgba
(
17
,
24
,
39
,
0.06
);
box-shadow
:
0
18px
34px
rgba
(
17
,
24
,
39
,
0.06
);
}
}
.conversation-shell
.sidebar-experts-entry
{
.sidebar-experts-entry
{
flex
:
0
0
calc
(
55%
-
6px
);
min-height
:
calc
(
55%
-
6px
);
display
:
grid
;
display
:
grid
;
grid-template-rows
:
minmax
(
0
,
1
fr
);
grid-template-rows
:
auto
auto
;
gap
:
10px
;
align-content
:
start
;
overflow
:
visible
;
}
}
.conversation-shell
.sidebar-section-fill
{
.sidebar-session-section
{
flex
:
0
0
calc
(
45%
-
6px
);
min-height
:
calc
(
45%
-
6px
);
display
:
grid
;
display
:
grid
;
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
overflow
:
hidden
;
}
.sidebar-digital-workers-title
{
min-height
:
24px
;
align-items
:
center
;
position
:
sticky
;
top
:
0
;
z-index
:
1
;
background
:
linear-gradient
(
180deg
,
rgba
(
255
,
255
,
255
,
0.98
),
rgba
(
247
,
251
,
250
,
0.96
));
}
}
.conversation-shell
.sidebar-expert-scroll
{
.sidebar-section-fill
{
display
:
grid
;
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
}
.sidebar-expert-scroll
{
min-height
:
0
;
min-height
:
0
;
overflow-y
:
auto
;
overflow-y
:
visible
;
overflow-x
:
hidden
;
overflow-x
:
hidden
;
padding-right
:
2px
;
padding-right
:
0
;
/* 继承自定义滚动条样式 */
/* 继承自定义滚动条样式 */
}
}
.conversation-shell
.sidebar-session-list
{
.expert-category-header
:focus-visible
,
.expert-category-expert-item
:focus-visible
{
outline
:
2px
solid
rgba
(
109
,
93
,
252
,
0.38
);
outline-offset
:
2px
;
}
.sidebar-session-list
{
min-height
:
0
;
min-height
:
0
;
overflow
:
auto
;
overflow
:
auto
;
padding-right
:
2px
;
padding-right
:
2px
;
scrollbar-width
:
thin
;
scrollbar-color
:
rgba
(
139
,
92
,
246
,
0.3
)
transparent
;
}
.sidebar-session-list
::-webkit-scrollbar-thumb
{
background
:
rgba
(
139
,
92
,
246
,
0.3
);
}
}
.conversation-shell
.conversation-main-layout
,
.conversation-shell
.conversation-main-layout
,
...
@@ -4881,6 +4911,34 @@ button.secondary {
...
@@ -4881,6 +4911,34 @@ button.secondary {
min-width
:
0
;
min-width
:
0
;
}
}
.conversation-shell
.expert-workspace-logo
{
width
:
44px
;
height
:
44px
;
flex
:
0
0
auto
;
display
:
grid
;
place-items
:
center
;
border-radius
:
14px
;
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0.72
);
background
:
rgba
(
255
,
255
,
255
,
0.78
);
box-shadow
:
0
14px
28px
rgba
(
15
,
23
,
42
,
0.1
);
}
.conversation-shell
.expert-workspace-logo
svg
{
width
:
26px
;
height
:
26px
;
display
:
block
;
}
.conversation-shell
.expert-workspace-logo-xiaohongshu
{
background
:
linear-gradient
(
135deg
,
rgba
(
255
,
245
,
245
,
0.96
),
rgba
(
255
,
228
,
230
,
0.9
));
box-shadow
:
0
14px
28px
rgba
(
239
,
68
,
68
,
0.16
);
}
.conversation-shell
.expert-workspace-logo-douyin
{
background
:
linear-gradient
(
135deg
,
rgba
(
236
,
254
,
255
,
0.96
),
rgba
(
244
,
244
,
255
,
0.92
));
box-shadow
:
0
14px
28px
rgba
(
109
,
125
,
255
,
0.16
);
}
.conversation-shell
.conversation-panel-actions
{
.conversation-shell
.conversation-panel-actions
{
display
:
inline-flex
;
display
:
inline-flex
;
align-items
:
center
;
align-items
:
center
;
...
@@ -5022,6 +5080,9 @@ button.secondary {
...
@@ -5022,6 +5080,9 @@ button.secondary {
}
}
.conversation-shell
.composer-shell
{
.conversation-shell
.composer-shell
{
--composer-textarea-height
:
96px
;
--composer-textarea-min-height
:
48px
;
--composer-textarea-max-height
:
188px
;
position
:
relative
;
position
:
relative
;
gap
:
0
;
gap
:
0
;
padding
:
14px
0
0
;
padding
:
14px
0
0
;
...
@@ -5035,6 +5096,51 @@ button.secondary {
...
@@ -5035,6 +5096,51 @@ button.secondary {
border-top
:
1px
solid
rgba
(
141
,
156
,
255
,
0.1
);
/* 浅紫色边框 */
border-top
:
1px
solid
rgba
(
141
,
156
,
255
,
0.1
);
/* 浅紫色边框 */
}
}
.conversation-shell
.composer-shell.resizing
,
.conversation-shell
.composer-shell.resizing
*
{
cursor
:
ns-resize
;
}
.conversation-shell
.composer-resize-handle
{
position
:
absolute
;
top
:
0
;
left
:
0
;
right
:
0
;
height
:
16px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
border
:
0
;
background
:
transparent
;
cursor
:
ns-resize
;
touch-action
:
none
;
user-select
:
none
;
z-index
:
2
;
}
.conversation-shell
.composer-resize-handle
span
{
width
:
48px
;
height
:
4px
;
border-radius
:
999px
;
background
:
rgba
(
109
,
93
,
252
,
0.2
);
box-shadow
:
0
1px
0
rgba
(
255
,
255
,
255
,
0.82
);
opacity
:
0.72
;
transition
:
background
160ms
ease
,
opacity
160ms
ease
,
transform
160ms
ease
;
}
.conversation-shell
.composer-resize-handle
:hover
span
,
.conversation-shell
.composer-resize-handle
:focus-visible
span
,
.conversation-shell
.composer-shell.resizing
.composer-resize-handle
span
{
background
:
rgba
(
109
,
93
,
252
,
0.38
);
opacity
:
1
;
transform
:
scaleX
(
1.12
);
}
.conversation-shell
.composer-resize-handle
:focus-visible
{
outline
:
2px
solid
rgba
(
109
,
93
,
252
,
0.32
);
outline-offset
:
2px
;
}
.conversation-shell
.composer-surface
{
.conversation-shell
.composer-surface
{
display
:
grid
;
display
:
grid
;
gap
:
12px
;
gap
:
12px
;
...
@@ -5064,8 +5170,9 @@ button.secondary {
...
@@ -5064,8 +5170,9 @@ button.secondary {
.conversation-shell
.composer-field
textarea
,
.conversation-shell
.composer-field
textarea
,
.conversation-shell
.composer-textarea
{
.conversation-shell
.composer-textarea
{
min-height
:
72px
;
height
:
var
(
--composer-textarea-height
);
max-height
:
188px
;
min-height
:
var
(
--composer-textarea-min-height
);
max-height
:
var
(
--composer-textarea-max-height
);
padding
:
0
;
padding
:
0
;
border
:
0
;
border
:
0
;
border-radius
:
0
;
border-radius
:
0
;
...
@@ -5252,6 +5359,71 @@ button.secondary {
...
@@ -5252,6 +5359,71 @@ button.secondary {
padding
:
32px
;
padding
:
32px
;
}
}
@media
(
max-width
:
1440px
),
(
max-height
:
820px
)
{
.shell
,
.conversation-shell
{
grid-template-columns
:
clamp
(
232px
,
18vw
,
260px
)
minmax
(
0
,
1
fr
);
}
.conversation-sidebar-layout
{
padding
:
14px
12px
;
gap
:
10px
;
}
.sidebar-top
{
gap
:
8px
;
}
.nav-list
{
gap
:
8px
;
}
.nav-item
{
height
:
40px
;
}
.conversation-sidebar-action
.conversation-new-session
{
min-height
:
40px
;
padding
:
0
12px
;
}
.sidebar-bottom
{
gap
:
10px
;
}
.sidebar-experts-entry
{
flex-basis
:
calc
(
55%
-
5px
);
min-height
:
calc
(
55%
-
5px
);
padding
:
10px
;
gap
:
8px
;
}
.sidebar-session-section
{
flex-basis
:
calc
(
45%
-
5px
);
min-height
:
calc
(
45%
-
5px
);
padding
:
10px
;
}
.expert-category-list
{
gap
:
4px
;
}
.expert-category-header
{
min-height
:
36px
;
padding
:
0
8px
;
}
.expert-category-expert-item
{
min-height
:
32px
;
padding-top
:
5px
;
padding-bottom
:
5px
;
}
.sidebar-session-card
{
min-height
:
40px
;
}
}
@media
(
max-width
:
960px
)
{
@media
(
max-width
:
960px
)
{
.conversation-shell
.conversation-workspace
{
.conversation-shell
.conversation-workspace
{
padding
:
18px
;
padding
:
18px
;
...
@@ -5510,7 +5682,6 @@ button.secondary {
...
@@ -5510,7 +5682,6 @@ button.secondary {
.nav-item.active
{
.nav-item.active
{
background
:
rgba
(
124
,
58
,
237
,
0.15
);
background
:
rgba
(
124
,
58
,
237
,
0.15
);
border-left
:
3px
solid
var
(
--ui-color-primary
);
}
}
.nav-item.active
::before
{
.nav-item.active
::before
{
...
...
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