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
8645a0f7
Commit
8645a0f7
authored
May 15, 2026
by
edy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): refine task workbench output panel
parent
d60ecae9
Pipeline
#18461
failed
Changes
5
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
507 additions
and
448 deletions
+507
-448
AppSidebar.tsx
apps/ui/src/features/shell/AppSidebar.tsx
+1
-1
TaskPanelView.tsx
apps/ui/src/features/tasks/TaskPanelView.tsx
+156
-250
taskPanelData.ts
apps/ui/src/features/tasks/taskPanelData.ts
+66
-9
tasks.css
apps/ui/src/styles/tasks.css
+280
-188
index.ts
packages/shared-types/src/index.ts
+4
-0
No files found.
apps/ui/src/features/shell/AppSidebar.tsx
View file @
8645a0f7
...
...
@@ -70,7 +70,7 @@ export function AppSidebar({
<
nav
className=
"nav-list"
aria
-
label=
"主导航"
>
{
[
{
id
:
"chat"
as
const
,
label
:
"对话"
},
{
id
:
"tasks"
as
const
,
label
:
"
任务面板
"
},
{
id
:
"tasks"
as
const
,
label
:
"
工作台
"
},
{
id
:
"knowledge"
as
const
,
label
:
ui
.
knowledge
},
{
id
:
"plugins"
as
const
,
label
:
ui
.
plugins
},
{
id
:
"settings"
as
const
,
label
:
ui
.
settings
}
...
...
apps/ui/src/features/tasks/TaskPanelView.tsx
View file @
8645a0f7
import
{
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
import
{
createPortal
}
from
"react-dom"
import
type
{
TaskPanelArtifact
,
TaskPanelItem
,
TaskPanelStatus
}
from
"@qjclaw/shared-types"
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
import
type
{
TaskPanelArtifact
,
TaskPanelItem
}
from
"@qjclaw/shared-types"
import
{
renderExpertIcon
}
from
"../../components/icons/AppIcons"
import
{
Panel
}
from
"../../components/ui/Panel"
import
{
ScrollArea
}
from
"../../components/ui/ScrollArea"
import
type
{
ExpertVisualKey
}
from
"../shell/ExpertTree"
import
{
getDefaultTaskPanelDate
,
loadTaskPanelItems
}
from
"./taskPanelData"
const
statusLabels
:
Record
<
TaskPanelStatus
,
string
>
=
{
pending
:
"待处理"
,
running
:
"执行中"
,
completed
:
"已完成"
,
failed
:
"失败"
}
import
{
getDefaultTaskPanelDate
,
loadTaskPanelItems
,
summarizeTaskPanelItems
}
from
"./taskPanelData"
function
resolveTaskExpertIconKey
(
expertName
:
string
):
ExpertVisualKey
{
const
seed
=
expertName
.
toLowerCase
()
...
...
@@ -55,70 +47,109 @@ function resolveTaskExpertIconKey(expertName: string): ExpertVisualKey {
return
"general"
}
function
TaskStatus
({
item
}:
{
item
:
TaskPanelItem
})
{
interface
TaskPanelOutputItem
{
artifact
:
TaskPanelArtifact
task
:
TaskPanelItem
}
function
TaskPanelMetricIcon
({
kind
}:
{
kind
:
"credits"
|
"messages"
|
"artifacts"
|
"employees"
})
{
if
(
kind
===
"credits"
)
{
return
(
<
div
className=
{
"task-panel-status task-panel-status-"
+
item
.
status
}
>
<
span
className=
"task-panel-status-icon"
title=
{
item
.
statusDetail
}
aria
-
hidden=
"true"
>
{
item
.
status
===
"completed"
?
<
span
>
✓
</
span
>
:
null
}
{
item
.
status
===
"running"
?
(
<
span
className=
"task-panel-running-dots"
aria
-
hidden=
"true"
>
<
span
/>
<
span
/>
<
span
/>
</
span
>
)
:
null
}
{
item
.
status
===
"pending"
?
<
span
>
•
</
span
>
:
null
}
{
item
.
status
===
"failed"
?
<
span
>
!
</
span
>
:
null
}
</
span
>
<
strong
className=
"task-panel-status-text"
title=
{
item
.
statusDetail
}
>
{
statusLabels
[
item
.
status
]
}
</
strong
>
</
div
>
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M12 3.75 14.3 8.4l5.1.74-3.7 3.6.87 5.08L12 15.42l-4.57 2.4.87-5.08-3.7-3.6 5.1-.74L12 3.75Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.7"
/>
</
svg
>
)
}
if
(
kind
===
"messages"
)
{
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M5.25 6.75A2.75 2.75 0 0 1 8 4h8a2.75 2.75 0 0 1 2.75 2.75v5.5A2.75 2.75 0 0 1 16 15h-3.3l-3.25 3.05c-.44.41-1.15.1-1.15-.51V15H8a2.75 2.75 0 0 1-2.75-2.75v-5.5Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.7"
/>
<
path
d=
"M8.75 8.4h6.5M8.75 11.25h4.5"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeWidth=
"1.6"
/>
</
svg
>
)
}
if
(
kind
===
"artifacts"
)
{
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M7 4.25h6.35L18 8.9v8.85A2.25 2.25 0 0 1 15.75 20H7a2.25 2.25 0 0 1-2.25-2.25V6.5A2.25 2.25 0 0 1 7 4.25Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.7"
/>
<
path
d=
"M13.25 4.5v3.35c0 .7.57 1.27 1.27 1.27h3.25M8.3 12.2h6.6M8.3 15.45h4.4"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.55"
/>
</
svg
>
)
}
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M8.75 11.25a3.25 3.25 0 1 1 0-6.5 3.25 3.25 0 0 1 0 6.5Zm6.75.5a2.75 2.75 0 1 1 0-5.5 2.75 2.75 0 0 1 0 5.5Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.65"
/>
<
path
d=
"M3.9 18.75a4.85 4.85 0 0 1 9.7 0m-.55-1.3a4.05 4.05 0 0 1 7.05 1.3"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeWidth=
"1.65"
/>
</
svg
>
)
}
function
TaskArtifactList
({
artifacts
,
copiedArtifactId
,
onCopy
}:
{
artifacts
:
TaskPanelArtifact
[]
copiedArtifactId
:
string
onCopy
:
(
artifactId
:
string
,
artifactUrl
:
string
)
=>
void
})
{
function
TaskPanelOutputIcon
({
artifact
}:
{
artifact
:
TaskPanelArtifact
})
{
const
normalizedKind
=
[
artifact
.
kind
,
artifact
.
name
,
artifact
.
url
].
filter
(
Boolean
).
join
(
" "
).
toLowerCase
()
if
(
/视频|video|mp4|mov|m4v|avi|webm/
.
test
(
normalizedKind
))
{
return
(
<
ul
className=
"task-panel-artifact-list"
aria
-
label=
"产物清单"
>
{
artifacts
.
map
((
artifact
)
=>
(
<
li
key=
{
artifact
.
id
}
className=
{
copiedArtifactId
===
artifact
.
id
?
"task-panel-artifact-item-copied"
:
undefined
}
title=
{
artifact
.
url
??
artifact
.
name
}
>
<
span
className=
"task-panel-artifact-name"
title=
{
artifact
.
name
}
>
{
artifact
.
name
}
</
span
>
{
artifact
.
url
?
(
<
button
type=
"button"
className=
"task-panel-artifact-url"
title=
{
artifact
.
url
}
onClick=
{
()
=>
onCopy
(
artifact
.
id
,
artifact
.
url
??
""
)
}
>
{
artifact
.
url
}
</
button
>
)
:
null
}
{
copiedArtifactId
===
artifact
.
id
?
(
<
span
className=
"task-panel-artifact-copied"
aria
-
live=
"polite"
>
已复制
</
span
>
)
:
null
}
</
li
>
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M5.25 7.25A2.25 2.25 0 0 1 7.5 5h6.25A2.25 2.25 0 0 1 16 7.25v.55l2.55-1.45c.74-.42 1.65.12 1.65.97v9.36c0 .85-.91 1.39-1.65.97L16 16.2v.55A2.25 2.25 0 0 1 13.75 19H7.5a2.25 2.25 0 0 1-2.25-2.25v-9.5Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.7"
/>
<
path
d=
"m9.6 9.1 3.7 2.4-3.7 2.4V9.1Z"
fill=
"currentColor"
/>
</
svg
>
)
}
if
(
/表格|xlsx|csv|sheet/
.
test
(
normalizedKind
))
{
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M5.5 5.75A2.25 2.25 0 0 1 7.75 3.5h8.5a2.25 2.25 0 0 1 2.25 2.25v12.5a2.25 2.25 0 0 1-2.25 2.25h-8.5a2.25 2.25 0 0 1-2.25-2.25V5.75Z"
fill=
"none"
stroke=
"currentColor"
strokeWidth=
"1.7"
/>
<
path
d=
"M5.9 9.25h12.2M5.9 13h12.2M10 9.25v10.8"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeWidth=
"1.45"
/>
</
svg
>
)
}
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M7.25 3.75h6.1L18.75 9v9.25a2 2 0 0 1-2 2h-9.5a2 2 0 0 1-2-2V5.75a2 2 0 0 1 2-2Z"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.7"
/>
<
path
d=
"M13.2 4.1v3.7c0 .7.56 1.26 1.26 1.26h3.9M8.4 12.5h7.2M8.4 15.8h4.75"
fill=
"none"
stroke=
"currentColor"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.5"
/>
</
svg
>
)
}
function
TaskPanelStatCards
({
items
}:
{
items
:
TaskPanelItem
[]
})
{
const
summary
=
useMemo
(()
=>
summarizeTaskPanelItems
(
items
),
[
items
])
const
stats
=
[
{
key
:
"credits"
,
label
:
"今日消耗Credits"
,
value
:
summary
.
creditsUsed
.
toLocaleString
(
"zh-CN"
),
icon
:
"credits"
as
const
},
{
key
:
"messages"
,
label
:
"今日消息条数"
,
value
:
summary
.
messageCount
.
toLocaleString
(
"zh-CN"
),
icon
:
"messages"
as
const
},
{
key
:
"artifacts"
,
label
:
"今日产物数量"
,
value
:
summary
.
artifactCount
.
toLocaleString
(
"zh-CN"
),
icon
:
"artifacts"
as
const
},
{
key
:
"employees"
,
label
:
"参与员工数量"
,
value
:
summary
.
employeeCount
.
toLocaleString
(
"zh-CN"
),
icon
:
"employees"
as
const
}
]
return
(
<
section
className=
"task-panel-stats"
aria
-
label=
"任务统计"
>
{
stats
.
map
((
stat
)
=>
(
<
article
key=
{
stat
.
key
}
className=
{
"task-panel-stat-card task-panel-stat-card-"
+
stat
.
key
}
>
<
span
className=
"task-panel-stat-icon"
aria
-
hidden=
"true"
>
<
TaskPanelMetricIcon
kind=
{
stat
.
icon
}
/>
</
span
>
<
strong
className=
"task-panel-stat-value"
>
{
stat
.
value
}
</
strong
>
<
span
className=
"task-panel-stat-label"
>
{
stat
.
label
}
</
span
>
</
article
>
))
}
</
ul
>
</
section
>
)
}
function
TaskArtifacts
({
item
}:
{
item
:
TaskPanelItem
})
{
function
formatTaskPanelOutputTime
(
task
:
TaskPanelItem
)
{
const
dateText
=
task
.
date
.
replaceAll
(
"-"
,
"/"
)
const
timeText
=
task
.
updatedAt
?.
trim
()
??
"00:00:00"
const
timeMatch
=
timeText
.
match
(
/^
(\d{1,2})
:
(\d{2})(?:
:
(\d{2}))?
$/
)
if
(
!
timeMatch
)
{
return
`
${
dateText
}
${
timeText
}
`
}
const
hour
=
timeMatch
[
1
].
padStart
(
2
,
"0"
)
const
minute
=
timeMatch
[
2
]
const
second
=
timeMatch
[
3
]
??
"00"
return
`
${
dateText
}
${
hour
}
:
${
minute
}
:
${
second
}
`
}
function
TaskPanelOutputList
({
outputs
}:
{
outputs
:
TaskPanelOutputItem
[]
})
{
const
[
copiedArtifactId
,
setCopiedArtifactId
]
=
useState
(
""
)
const
[
isMenuOpen
,
setIsMenuOpen
]
=
useState
(
false
)
const
[
popoverPosition
,
setPopoverPosition
]
=
useState
<
{
top
:
number
;
left
:
number
;
width
:
number
;
maxHeight
:
number
}
|
null
>
(
null
)
const
menuRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
)
const
triggerRef
=
useRef
<
HTMLButtonElement
|
null
>
(
null
)
const
popoverRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
)
const
copiedTimerRef
=
useRef
<
number
|
null
>
(
null
)
useEffect
(()
=>
{
...
...
@@ -129,78 +160,9 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
}
},
[])
const
updatePopoverPosition
=
useCallback
(()
=>
{
const
triggerElement
=
triggerRef
.
current
if
(
!
triggerElement
)
{
return
}
const
margin
=
16
const
gap
=
8
const
triggerRect
=
triggerElement
.
getBoundingClientRect
()
const
width
=
Math
.
min
(
520
,
Math
.
max
(
280
,
window
.
innerWidth
-
margin
*
2
))
const
left
=
Math
.
min
(
Math
.
max
(
triggerRect
.
left
,
margin
),
window
.
innerWidth
-
width
-
margin
)
const
belowTop
=
triggerRect
.
bottom
+
gap
const
spaceBelow
=
window
.
innerHeight
-
belowTop
-
margin
const
spaceAbove
=
triggerRect
.
top
-
margin
-
gap
const
preferBelow
=
spaceBelow
>=
160
||
spaceBelow
>=
spaceAbove
const
maxHeight
=
Math
.
min
(
220
,
Math
.
max
(
140
,
preferBelow
?
spaceBelow
:
spaceAbove
))
const
top
=
preferBelow
?
belowTop
:
Math
.
max
(
margin
,
triggerRect
.
top
-
gap
-
maxHeight
)
setPopoverPosition
({
top
,
left
,
width
,
maxHeight
})
},
[])
useEffect
(()
=>
{
setIsMenuOpen
(
false
)
setCopiedArtifactId
(
""
)
},
[
item
.
id
])
useEffect
(()
=>
{
if
(
!
isMenuOpen
)
{
return
}
const
handlePointerDown
=
(
event
:
PointerEvent
)
=>
{
const
menuElement
=
menuRef
.
current
const
popoverElement
=
popoverRef
.
current
if
(
event
.
target
instanceof
Node
&&
!
menuElement
?.
contains
(
event
.
target
)
&&
!
popoverElement
?.
contains
(
event
.
target
)
)
{
setIsMenuOpen
(
false
)
}
}
const
handleKeyDown
=
(
event
:
KeyboardEvent
)
=>
{
if
(
event
.
key
===
"Escape"
)
{
setIsMenuOpen
(
false
)
}
}
document
.
addEventListener
(
"pointerdown"
,
handlePointerDown
,
true
)
document
.
addEventListener
(
"keydown"
,
handleKeyDown
)
return
()
=>
{
document
.
removeEventListener
(
"pointerdown"
,
handlePointerDown
,
true
)
document
.
removeEventListener
(
"keydown"
,
handleKeyDown
)
}
},
[
isMenuOpen
])
useEffect
(()
=>
{
if
(
!
isMenuOpen
)
{
return
}
updatePopoverPosition
()
window
.
addEventListener
(
"resize"
,
updatePopoverPosition
)
window
.
addEventListener
(
"scroll"
,
updatePopoverPosition
,
true
)
return
()
=>
{
window
.
removeEventListener
(
"resize"
,
updatePopoverPosition
)
window
.
removeEventListener
(
"scroll"
,
updatePopoverPosition
,
true
)
}
},
[
isMenuOpen
,
updatePopoverPosition
])
},
[
outputs
])
const
copyArtifactUrl
=
async
(
artifactId
:
string
,
artifactUrl
:
string
)
=>
{
try
{
...
...
@@ -218,65 +180,61 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
}
}
if
(
!
item
.
artifacts
.
length
)
{
return
(
<
div
className=
"task-panel-artifact-center"
>
<
span
className=
"task-panel-muted"
>
暂无产物
</
span
>
<
section
className=
"task-panel-output-panel"
aria
-
label=
"内容产出列表"
>
<
div
className=
"task-panel-output-header"
>
<
div
className=
"task-panel-output-heading"
>
<
span
className=
"task-panel-output-heading-icon"
aria
-
hidden=
"true"
>
📦
</
span
>
<
h2
>
内容产出
</
h2
>
</
div
>
)
}
if
(
item
.
artifacts
.
length
<=
2
)
{
return
(
<
div
className=
"task-panel-artifact-center"
>
<
TaskArtifactList
artifacts=
{
item
.
artifacts
}
copiedArtifactId=
{
copiedArtifactId
}
onCopy=
{
(
artifactId
,
artifactUrl
)
=>
void
copyArtifactUrl
(
artifactId
,
artifactUrl
)
}
/>
</
div
>
)
}
return
(
<
div
className=
"task-panel-artifact-menu"
ref=
{
menuRef
}
>
<
div
className=
"task-panel-output-list"
>
{
outputs
.
length
?
outputs
.
map
(({
artifact
,
task
})
=>
(
<
article
key=
{
task
.
id
+
"-"
+
artifact
.
id
}
className=
"task-panel-output-item"
>
<
span
className=
"task-panel-output-icon"
aria
-
hidden=
"true"
>
<
TaskPanelOutputIcon
artifact=
{
artifact
}
/>
</
span
>
<
div
className=
"task-panel-output-main"
>
<
div
className=
"task-panel-output-title-row"
>
<
strong
title=
{
artifact
.
name
}
>
{
artifact
.
name
}
</
strong
>
<
span
className=
"task-panel-output-kind"
>
{
artifact
.
kind
??
"产物"
}
</
span
>
</
div
>
<
p
>
{
artifact
.
summary
??
task
.
taskTitle
}
</
p
>
{
artifact
.
url
?
(
<
div
className=
"task-panel-output-url-row"
>
<
button
ref=
{
triggerRef
}
type=
"button"
className=
"task-panel-artifact-trigger"
aria
-
haspopup=
"menu"
aria
-
expanded=
{
isMenuOpen
}
aria
-
controls=
{
item
.
id
+
"-artifact-menu"
}
onClick=
{
()
=>
{
if
(
!
isMenuOpen
)
{
updatePopoverPosition
()
}
setIsMenuOpen
((
current
)
=>
!
current
)
}
}
className=
"task-panel-output-url"
title=
{
artifact
.
url
}
onClick=
{
()
=>
void
copyArtifactUrl
(
artifact
.
id
,
artifact
.
url
??
""
)
}
>
{
item
.
artifacts
.
length
}
个产物
{
artifact
.
url
}
</
button
>
{
isMenuOpen
&&
popoverPosition
?
createPortal
((
<
div
ref=
{
popoverRef
}
className=
"task-panel-artifact-popover"
id=
{
item
.
id
+
"-artifact-menu"
}
role=
"menu"
style=
{
{
top
:
popoverPosition
.
top
,
left
:
popoverPosition
.
left
,
width
:
popoverPosition
.
width
,
maxHeight
:
popoverPosition
.
maxHeight
}
}
>
<
TaskArtifactList
artifacts=
{
item
.
artifacts
}
copiedArtifactId=
{
copiedArtifactId
}
onCopy=
{
(
artifactId
,
artifactUrl
)
=>
void
copyArtifactUrl
(
artifactId
,
artifactUrl
)
}
/>
{
copiedArtifactId
===
artifact
.
id
?
(
<
span
className=
"task-panel-artifact-copied"
aria
-
live=
"polite"
>
✅已复制
</
span
>
)
:
null
}
</
div
>
),
document
.
body
)
:
null
}
)
:
null
}
</
div
>
<
div
className=
"task-panel-output-meta"
>
<
span
className=
{
"task-panel-expert-icon task-panel-expert-icon-"
+
resolveTaskExpertIconKey
(
task
.
expertName
)
}
aria
-
hidden=
"true"
>
{
renderExpertIcon
(
resolveTaskExpertIconKey
(
task
.
expertName
))
}
</
span
>
<
div
>
<
strong
title=
{
task
.
expertName
}
>
{
task
.
expertName
}
</
strong
>
<
span
title=
{
task
.
taskTitle
}
>
{
formatTaskPanelOutputTime
(
task
)
}
·
{
task
.
taskTitle
}
</
span
>
</
div
>
</
div
>
</
article
>
))
:
(
<
div
className=
"empty-state task-panel-state task-panel-output-empty"
>
当前日期暂无内容产出
</
div
>
)
}
</
div
>
</
section
>
)
}
...
...
@@ -299,7 +257,6 @@ function TaskPanelLoadingState() {
export
function
TaskPanelView
()
{
const
[
selectedDate
,
setSelectedDate
]
=
useState
(
getDefaultTaskPanelDate
)
const
[
items
,
setItems
]
=
useState
<
TaskPanelItem
[]
>
([])
const
[
selectedTaskIds
,
setSelectedTaskIds
]
=
useState
<
Record
<
string
,
string
>>
({})
const
[
loading
,
setLoading
]
=
useState
(
true
)
const
[
errorText
,
setErrorText
]
=
useState
(
""
)
const
[
greetingText
,
setGreetingText
]
=
useState
(
""
)
...
...
@@ -353,22 +310,9 @@ export function TaskPanelView() {
const
displayDate
=
useMemo
(()
=>
selectedDate
.
replaceAll
(
"-"
,
"/"
),
[
selectedDate
])
const
displayMonth
=
useMemo
(()
=>
`
${
Number
(
selectedDate
.
slice
(
5
,
7
))}
月`
,
[
selectedDate
])
const
expertRows
=
useMemo
(()
=>
{
const
groups
=
new
Map
<
string
,
TaskPanelItem
[]
>
()
for
(
const
item
of
items
)
{
const
groupItems
=
groups
.
get
(
item
.
expertName
)
if
(
groupItems
)
{
groupItems
.
push
(
item
)
}
else
{
groups
.
set
(
item
.
expertName
,
[
item
])
}
}
return
Array
.
from
(
groups
,
([
expertName
,
tasks
])
=>
{
const
selectedTask
=
tasks
.
find
((
task
)
=>
task
.
id
===
selectedTaskIds
[
expertName
])
??
tasks
[
0
]
return
{
expertName
,
tasks
,
selectedTask
}
})
},
[
items
,
selectedTaskIds
])
const
outputItems
=
useMemo
<
TaskPanelOutputItem
[]
>
(()
=>
{
return
items
.
flatMap
((
task
)
=>
task
.
artifacts
.
map
((
artifact
)
=>
({
artifact
,
task
})))
},
[
items
])
return
(
<
div
className=
"page-stack task-panel-page-stack"
>
...
...
@@ -417,47 +361,9 @@ export function TaskPanelView() {
{
!
loading
&&
errorText
?
<
div
className=
"notice error task-panel-state"
role=
"alert"
>
{
errorText
}
</
div
>
:
null
}
{
!
loading
&&
!
errorText
&&
!
items
.
length
?
<
div
className=
"empty-state task-panel-state"
>
当天暂无任务
</
div
>
:
null
}
{
!
loading
&&
!
errorText
&&
items
.
length
?
(
<
div
className=
"task-panel-table"
role=
"table"
aria
-
label=
"任务面板"
>
<
div
className=
"task-panel-row task-panel-row-head"
role=
"row"
>
<
div
role=
"columnheader"
>
数字员工
</
div
>
<
div
role=
"columnheader"
>
任务列表
</
div
>
<
div
role=
"columnheader"
>
执行状态
</
div
>
<
div
role=
"columnheader"
>
产物清单
</
div
>
</
div
>
{
expertRows
.
map
((
row
)
=>
(
<
article
key=
{
row
.
expertName
}
className=
"task-panel-row"
role=
"row"
>
<
div
className=
"task-panel-expert-cell"
role=
"cell"
>
<
span
className=
{
"task-panel-expert-icon task-panel-expert-icon-"
+
resolveTaskExpertIconKey
(
row
.
expertName
)
}
aria
-
hidden=
"true"
>
{
renderExpertIcon
(
resolveTaskExpertIconKey
(
row
.
expertName
))
}
</
span
>
<
strong
title=
{
row
.
expertName
}
>
{
row
.
expertName
}
</
strong
>
</
div
>
<
div
className=
"task-panel-task-cell"
role=
"cell"
>
<
select
aria
-
label=
{
row
.
expertName
+
"任务列表"
}
title=
{
row
.
selectedTask
.
taskTitle
}
value=
{
row
.
selectedTask
.
id
}
onChange=
{
(
event
)
=>
{
const
selectedTaskId
=
event
.
currentTarget
.
value
setSelectedTaskIds
((
current
)
=>
({
...
current
,
[
row
.
expertName
]:
selectedTaskId
}))
}
}
>
{
row
.
tasks
.
map
((
task
)
=>
(
<
option
key=
{
task
.
id
}
title=
{
task
.
taskTitle
}
value=
{
task
.
id
}
>
{
task
.
taskTitle
}
</
option
>
))
}
</
select
>
</
div
>
<
div
role=
"cell"
>
<
TaskStatus
item=
{
row
.
selectedTask
}
/>
</
div
>
<
div
role=
"cell"
>
<
TaskArtifacts
item=
{
row
.
selectedTask
}
/>
</
div
>
</
article
>
))
}
<
div
className=
"task-panel-content"
>
<
TaskPanelStatCards
items=
{
items
}
/>
<
TaskPanelOutputList
outputs=
{
outputItems
}
/>
</
div
>
)
:
null
}
</
ScrollArea
>
...
...
apps/ui/src/features/tasks/taskPanelData.ts
View file @
8645a0f7
import
type
{
TaskPanelItem
}
from
"@qjclaw/shared-types"
export
interface
TaskPanelSummary
{
creditsUsed
:
number
messageCount
:
number
artifactCount
:
number
employeeCount
:
number
}
function
toDateInputValue
(
date
:
Date
)
{
const
year
=
date
.
getFullYear
()
const
month
=
String
(
date
.
getMonth
()
+
1
).
padStart
(
2
,
"0"
)
...
...
@@ -25,12 +32,15 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
taskTitle
:
"整理本周选题方向与发布节奏"
,
status
:
"running"
,
statusDetail
:
"正在汇总账号定位、目标人群和栏目节奏"
,
creditsUsed
:
1280
,
messageCount
:
36
,
updatedAt
:
"10:42"
,
artifacts
:
[
{
id
:
"artifact-content-outline"
,
name
:
"选题规划草稿.md"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/选题规划草稿.md"
},
{
id
:
"artifact-content-calendar"
,
name
:
"发布日历.xlsx"
,
kind
:
"表格"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/发布日历.xlsx"
},
{
id
:
"artifact-content-persona"
,
name
:
"目标人群画像.md"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/目标人群画像.md"
},
{
id
:
"artifact-content-topics"
,
name
:
"栏目选题池.csv"
,
kind
:
"表格"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/栏目选题池.csv"
},
{
id
:
"artifact-content-brief"
,
name
:
"账号定位简报.pdf"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/账号定位简报.pdf"
}
{
id
:
"artifact-content-outline"
,
name
:
"选题规划草稿.md"
,
kind
:
"文档"
,
summary
:
"本周内容主线、栏目节奏与素材需求草案。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/选题规划草稿.md"
},
{
id
:
"artifact-content-calendar"
,
name
:
"发布日历.xlsx"
,
kind
:
"表格"
,
summary
:
"按平台拆分的发布排期与负责人视图。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/发布日历.xlsx"
},
{
id
:
"artifact-content-persona"
,
name
:
"目标人群画像.md"
,
kind
:
"文档"
,
summary
:
"核心受众痛点、决策因素与内容偏好。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/目标人群画像.md"
},
{
id
:
"artifact-content-topics"
,
name
:
"栏目选题池.csv"
,
kind
:
"表格"
,
summary
:
"可复用选题、关键词和参考链接集合。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/栏目选题池.csv"
},
{
id
:
"artifact-content-brief"
,
name
:
"账号定位简报.pdf"
,
kind
:
"文档"
,
summary
:
"账号定位、差异化表达和近期目标摘要。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/账号定位简报.pdf"
}
]
},
{
...
...
@@ -40,8 +50,11 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
taskTitle
:
"复盘昨日内容表现"
,
status
:
"completed"
,
statusDetail
:
"已完成互动数据摘要与优化建议"
,
creditsUsed
:
640
,
messageCount
:
18
,
updatedAt
:
"09:18"
,
artifacts
:
[
{
id
:
"artifact-content-review"
,
name
:
"昨日内容复盘与下轮优化建议.md"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/昨日内容复盘与下轮优化建议.md"
}
{
id
:
"artifact-content-review"
,
name
:
"昨日内容复盘与下轮优化建议.md"
,
kind
:
"文档"
,
summary
:
"互动、转化和评论反馈的复盘结论。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/昨日内容复盘与下轮优化建议.md"
}
]
},
{
...
...
@@ -51,8 +64,11 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
taskTitle
:
"生成知乎回答结构"
,
status
:
"completed"
,
statusDetail
:
"已完成回答大纲与首版正文"
,
creditsUsed
:
920
,
messageCount
:
24
,
updatedAt
:
"11:05"
,
artifacts
:
[
{
id
:
"artifact-zhihu-answer"
,
name
:
"知乎回答初稿.md"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/zhihu/知乎回答初稿.md"
}
{
id
:
"artifact-zhihu-answer"
,
name
:
"知乎回答初稿.md"
,
kind
:
"文档"
,
summary
:
"问题拆解、回答结构和首版正文内容。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/zhihu/知乎回答初稿.md"
}
]
},
{
...
...
@@ -62,8 +78,11 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
taskTitle
:
"整理竞品问答素材"
,
status
:
"running"
,
statusDetail
:
"正在提取高赞回答结构和关键词"
,
creditsUsed
:
760
,
messageCount
:
21
,
updatedAt
:
"11:36"
,
artifacts
:
[
{
id
:
"artifact-zhihu-research"
,
name
:
"竞品问答素材汇总-长文件名用于验证省略显示效果.xlsx"
,
kind
:
"表格"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/zhihu/竞品问答素材汇总-长文件名用于验证省略显示效果.xlsx"
}
{
id
:
"artifact-zhihu-research"
,
name
:
"竞品问答素材汇总-长文件名用于验证省略显示效果.xlsx"
,
kind
:
"表格"
,
summary
:
"高赞回答结构、关键词和引用素材汇总。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/zhihu/竞品问答素材汇总-长文件名用于验证省略显示效果.xlsx"
}
]
},
{
...
...
@@ -73,6 +92,9 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
taskTitle
:
"筛选高意向线索名单"
,
status
:
"pending"
,
statusDetail
:
"等待线索表上传后开始处理"
,
creditsUsed
:
0
,
messageCount
:
4
,
updatedAt
:
"12:10"
,
artifacts
:
[]
},
{
...
...
@@ -82,12 +104,47 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
taskTitle
:
"生成活动海报文案"
,
status
:
"failed"
,
statusDetail
:
"素材包缺少主视觉图片"
,
creditsUsed
:
360
,
messageCount
:
12
,
updatedAt
:
"18:22"
,
artifacts
:
[
{
id
:
"artifact-poster-brief"
,
name
:
"活动海报文案草稿.txt"
,
kind
:
"文档"
,
summary
:
"活动主题、主标题和利益点文案草稿。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/poster/活动海报文案草稿.txt"
}
]
},
{
id
:
"mock-task-yesterday-leads"
,
date
:
toDateInputValue
(
addDays
(
new
Date
(),
-
1
)),
expertName
:
"平台精准线索专家"
,
taskTitle
:
"清洗昨日线索表"
,
status
:
"completed"
,
statusDetail
:
"已完成重复线索剔除和等级标注"
,
creditsUsed
:
520
,
messageCount
:
16
,
updatedAt
:
"17:40"
,
artifacts
:
[
{
id
:
"artifact-poster-brief"
,
name
:
"活动海报文案草稿.txt"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/poster/活动海报文案草稿.txt"
}
{
id
:
"artifact-yesterday-leads"
,
name
:
"昨日高意向线索清单.xlsx"
,
kind
:
"表格"
,
summary
:
"线索分级、跟进优先级和备注字段。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/leads/昨日高意向线索清单.xlsx"
},
{
id
:
"artifact-yesterday-followup"
,
name
:
"跟进话术建议.md"
,
kind
:
"文档"
,
summary
:
"按线索来源拆分的首轮沟通建议。"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/leads/跟进话术建议.md"
}
]
}
]
export
function
summarizeTaskPanelItems
(
items
:
TaskPanelItem
[]):
TaskPanelSummary
{
const
employeeCount
=
new
Set
(
items
.
map
((
item
)
=>
item
.
expertName
)).
size
const
summary
=
items
.
reduce
<
TaskPanelSummary
>
((
nextSummary
,
item
)
=>
{
nextSummary
.
creditsUsed
+=
item
.
creditsUsed
??
0
nextSummary
.
messageCount
+=
item
.
messageCount
??
0
nextSummary
.
artifactCount
+=
item
.
artifacts
.
length
return
nextSummary
},
{
creditsUsed
:
0
,
messageCount
:
0
,
artifactCount
:
0
,
employeeCount
})
return
summary
}
export
async
function
loadTaskPanelItems
(
date
:
string
):
Promise
<
TaskPanelItem
[]
>
{
return
mockTaskPanelItems
.
filter
((
item
)
=>
item
.
date
===
date
)
}
apps/ui/src/styles/tasks.css
View file @
8645a0f7
...
...
@@ -170,6 +170,12 @@
padding-right
:
2px
;
}
.task-panel-scroll
.scroll-area-content
{
display
:
grid
;
min-height
:
0
;
overflow
:
hidden
;
}
.task-panel-state
{
display
:
grid
;
min-height
:
220px
;
...
...
@@ -183,62 +189,89 @@
border-color
:
rgba
(
203
,
213
,
225
,
0.82
);
}
.task-panel-
table
{
.task-panel-
content
{
display
:
grid
;
gap
:
6px
;
min-width
:
860px
;
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
gap
:
12px
;
height
:
100%
;
min-width
:
0
;
min-height
:
0
;
}
.task-panel-
row
{
.task-panel-
stats
{
display
:
grid
;
grid-template-columns
:
150px
150px
138px
minmax
(
280px
,
1
fr
);
gap
:
12px
;
align-items
:
center
;
min-height
:
82px
;
padding
:
12px
14px
;
overflow
:
visible
;
grid-template-columns
:
repeat
(
4
,
minmax
(
0
,
1
fr
));
gap
:
10px
;
min-width
:
0
;
}
.task-panel-stat-card
{
display
:
grid
;
grid-template-columns
:
38px
minmax
(
0
,
1
fr
);
grid-template-rows
:
auto
auto
;
column-gap
:
10px
;
row-gap
:
3px
;
min-width
:
0
;
min-height
:
92px
;
padding
:
15px
;
border-radius
:
14px
;
border
:
1px
solid
rgba
(
219
,
234
,
254
,
0.92
);
background
:
linear-gradient
(
180deg
,
#ffffff
0%
,
#f7fbff
100%
);
box-shadow
:
0
10px
22px
rgba
(
15
,
23
,
42
,
0.045
);
}
.task-panel-stat-icon
{
display
:
grid
;
grid-row
:
1
/
span
2
;
width
:
38px
;
height
:
38px
;
place-items
:
center
;
border-radius
:
12px
;
border
:
1px
solid
rgba
(
226
,
232
,
240
,
0.96
);
background
:
rgba
(
255
,
255
,
255
,
0.88
);
box-shadow
:
0
8px
18px
rgba
(
15
,
23
,
42
,
0.035
);
transition
:
border-color
160ms
ease
,
background
160ms
ease
,
box-shadow
160ms
ease
,
transform
160ms
ease
;
color
:
#2563eb
;
background
:
#eff6ff
;
border
:
1px
solid
#bfdbfe
;
}
.task-panel-row-head
{
min-height
:
36px
;
align-items
:
center
;
padding
:
8px
14px
;
overflow
:
hidden
;
border-radius
:
10px
;
background
:
rgba
(
248
,
250
,
252
,
0.92
);
color
:
#64748b
;
font-size
:
12px
;
font-weight
:
700
;
box-shadow
:
none
;
.task-panel-stat-icon
svg
{
width
:
21px
;
height
:
21px
;
}
.task-panel-row
:not
(
.task-panel-row-head
)
:hover
,
.task-panel-row
:not
(
.task-panel-row-head
)
:focus-within
{
border-color
:
rgba
(
147
,
197
,
253
,
0.86
);
background
:
#ffffff
;
box-shadow
:
0
12px
26px
rgba
(
15
,
23
,
42
,
0.07
);
transform
:
translateY
(
-1px
);
.task-panel-stat-card-messages
.task-panel-stat-icon
,
.task-panel-stat-card-employees
.task-panel-stat-icon
{
color
:
#0f766e
;
background
:
#ecfdf5
;
border-color
:
#a7f3d0
;
}
.task-panel-row
>
div
{
min-width
:
0
;
.task-panel-stat-card-artifacts
.task-panel-stat-icon
{
color
:
#0891b2
;
background
:
#ecfeff
;
border-color
:
#a5f3fc
;
}
.task-panel-row
:not
(
.task-panel-row-head
)
>
div
:nth-child
(
3
)
{
padding-left
:
14px
;
.task-panel-stat-label
{
align-self
:
start
;
min-width
:
0
;
color
:
#64748b
;
font-size
:
12px
;
font-weight
:
700
;
line-height
:
1.4
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.task-panel-row
:not
(
.task-panel-row-head
)
>
div
:nth-child
(
4
)
{
display
:
flex
;
position
:
relative
;
align-items
:
center
;
justify-content
:
flex-start
;
padding-left
:
0
;
.task-panel-stat-value
{
align-self
:
end
;
min-width
:
0
;
color
:
#17253d
;
font-size
:
24px
;
font-weight
:
780
;
line-height
:
1.1
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.task-panel-expert-cell
{
...
...
@@ -300,41 +333,6 @@
white-space
:
nowrap
;
}
.task-panel-task-cell
{
display
:
flex
;
min-width
:
0
;
}
.task-panel-task-cell
select
{
width
:
10em
;
max-width
:
100%
;
min-width
:
0
;
min-height
:
34px
;
padding
:
0
34px
0
12px
;
border-radius
:
10px
;
border
:
1px
solid
#d8e1ef
;
background
:
#f8fafc
;
color
:
#1f2f49
;
font
:
inherit
;
font-size
:
13px
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
cursor
:
pointer
;
transition
:
border-color
160ms
ease
,
background
160ms
ease
,
box-shadow
160ms
ease
;
}
.task-panel-task-cell
select
:hover
,
.task-panel-task-cell
select
:focus-visible
{
border-color
:
#93c5fd
;
background
:
#ffffff
;
}
.task-panel-task-cell
select
:focus-visible
{
outline
:
2px
solid
rgba
(
37
,
99
,
235
,
0.26
);
outline-offset
:
2px
;
}
.task-panel-status
{
display
:
inline-flex
;
align-items
:
center
;
...
...
@@ -420,165 +418,254 @@
border
:
1px
solid
#fecaca
;
}
.task-panel-
artifact-list
{
.task-panel-
output-panel
{
display
:
grid
;
gap
:
5px
;
width
:
100%
;
margin
:
0
;
padding
:
0
;
list-style
:
none
;
min-width
:
0
;
overflow
:
visible
;
}
.task-panel-artifact-center
{
display
:
flex
;
width
:
100%
;
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
gap
:
10px
;
min-width
:
0
;
align-items
:
center
;
justify-content
:
flex-start
;
min-height
:
0
;
}
.task-panel-artifact-menu
{
position
:
relative
;
display
:
flex
;
width
:
100%
;
min-width
:
0
;
align-items
:
center
;
justify-content
:
flex-start
;
z-index
:
4
;
.task-panel-output-header
{
display
:
grid
;
padding
:
14px
16px
;
border-radius
:
14px
;
border
:
1px
solid
rgba
(
219
,
234
,
254
,
0.82
);
background
:
linear-gradient
(
180deg
,
#f8fbff
0%
,
#ffffff
100%
);
}
.task-panel-
artifact-trigger
{
.task-panel-
output-heading
{
display
:
inline-flex
;
min-height
:
32px
;
align-items
:
center
;
justify-content
:
center
;
padding
:
0
12px
;
border-radius
:
10px
;
border
:
1px
solid
#cfe0f5
;
background
:
#ffffff
;
color
:
#1f2f49
;
box-shadow
:
none
;
font
:
inherit
;
font-size
:
13px
;
font-weight
:
700
;
line-height
:
1
;
white-space
:
nowrap
;
cursor
:
pointer
;
gap
:
12px
;
min-width
:
0
;
}
.task-panel-artifact-trigger
:hover
,
.task-panel-artifact-trigger
:focus-visible
,
.task-panel-artifact-trigger
[
aria-expanded
=
"true"
]
{
border-color
:
#93c5fd
;
color
:
#1d4ed8
;
.task-panel-output-heading-icon
{
display
:
grid
;
width
:
42px
;
height
:
42px
;
flex
:
0
0
auto
;
place-items
:
center
;
border-radius
:
13px
;
color
:
#0f766e
;
background
:
linear-gradient
(
135deg
,
#ecfeff
0%
,
#ecfdf5
100%
);
border
:
1px
solid
#99f6e4
;
box-shadow
:
inset
0
1px
0
rgba
(
255
,
255
,
255
,
0.72
),
0
8px
16px
rgba
(
15
,
118
,
110
,
0.1
);
font-size
:
24px
;
line-height
:
1
;
}
.task-panel-artifact-trigger
:focus-visible
{
outline
:
2px
solid
rgba
(
37
,
99
,
235
,
0.28
);
outline-offset
:
2px
;
.task-panel-output-header
h2
{
margin
:
0
;
color
:
#17253d
;
font-size
:
16px
;
font-weight
:
780
;
line-height
:
1.35
;
}
.task-panel-artifact-popover
{
position
:
fixed
;
.task-panel-output-list
{
display
:
grid
;
align-content
:
start
;
gap
:
8px
;
min-width
:
0
;
min-height
:
0
;
overflow-y
:
auto
;
padding
:
6px
;
border-radius
:
12px
;
border
:
1px
solid
rgba
(
203
,
213
,
225
,
0.96
);
background
:
#ffffff
;
box-shadow
:
0
18px
42px
rgba
(
15
,
23
,
42
,
0.14
);
transform
:
none
;
z-index
:
1200
;
}
.task-panel-artifact-popover
.task-panel-artifact-list
{
width
:
100%
;
overscroll-behavior
:
contain
;
padding-right
:
4px
;
}
.task-panel-
artifact-list
li
{
.task-panel-
output-item
{
display
:
grid
;
position
:
relative
;
grid-template-columns
:
minmax
(
96px
,
0.72
fr
)
minmax
(
0
,
1.28
fr
)
auto
;
grid-template-columns
:
30px
minmax
(
0
,
1
fr
)
minmax
(
260px
,
0.46
fr
);
gap
:
12px
;
align-items
:
center
;
gap
:
8px
;
min-width
:
0
;
min-height
:
32
px
;
padding
:
5px
8
px
;
border
-radius
:
9px
;
background
:
rgba
(
2
48
,
250
,
252
,
0.88
);
bo
rder
:
1px
solid
transparent
;
transition
:
border-color
160ms
ease
,
background
160ms
ease
;
padding
:
14
px
;
border-radius
:
14
px
;
border
:
1px
solid
rgba
(
226
,
232
,
240
,
0.96
)
;
background
:
rgba
(
2
55
,
255
,
255
,
0.92
);
bo
x-shadow
:
0
8px
18px
rgba
(
15
,
23
,
42
,
0.035
)
;
transition
:
border-color
160ms
ease
,
background
160ms
ease
,
box-shadow
160ms
ease
,
transform
160ms
ease
;
}
.task-panel-
artifact-list
li
:hover
,
.task-panel-
artifact-list
li
:focus-within
{
border-color
:
#dbeafe
;
.task-panel-
output-item
:hover
,
.task-panel-
output-item
:focus-within
{
border-color
:
rgba
(
147
,
197
,
253
,
0.86
)
;
background
:
#ffffff
;
box-shadow
:
0
12px
26px
rgba
(
15
,
23
,
42
,
0.07
);
transform
:
translateY
(
-1px
);
}
.task-panel-output-icon
{
display
:
grid
;
width
:
30px
;
height
:
30px
;
place-items
:
center
;
border-radius
:
10px
;
color
:
#0f766e
;
background
:
#ecfdf5
;
border
:
1px
solid
#a7f3d0
;
}
.task-panel-artifact-list
li
.task-panel-artifact-item-copied
{
padding-right
:
8px
;
.task-panel-output-icon
svg
{
width
:
18px
;
height
:
18px
;
}
.task-panel-artifact-name
,
.task-panel-artifact-url
{
.task-panel-output-main
,
.task-panel-output-meta
,
.task-panel-output-meta
>
div
{
min-width
:
0
;
font-size
:
12.5px
;
line-height
:
1.5
;
}
.task-panel-output-main
{
display
:
grid
;
gap
:
5px
;
}
.task-panel-output-title-row
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
min-width
:
0
;
}
.task-panel-output-title-row
strong
{
min-width
:
0
;
color
:
#17253d
;
font-size
:
14px
;
font-weight
:
760
;
line-height
:
1.35
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.task-panel-artifact-name
{
color
:
#1f2f49
;
.task-panel-output-kind
{
flex
:
0
0
auto
;
padding
:
3px
8px
;
border-radius
:
999px
;
color
:
#0f766e
;
background
:
#ccfbf1
;
font-size
:
12px
;
font-weight
:
750
;
line-height
:
1
;
white-space
:
nowrap
;
}
.task-panel-artifact-url
{
color
:
#60728c
;
.task-panel-output-main
p
{
margin
:
0
;
color
:
#52657f
;
font-size
:
12.5px
;
font-weight
:
600
;
line-height
:
1.5
;
}
.task-panel-output-url-row
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
8px
;
min-width
:
0
;
justify-self
:
start
;
max-width
:
100%
;
}
.task-panel-output-url
{
flex
:
1
1
auto
;
min-width
:
0
;
min-height
:
24px
;
padding
:
0
;
border
:
0
;
border-radius
:
6px
;
background
:
transparent
;
color
:
#60728c
;
font
:
inherit
;
font-size
:
12.5px
;
font-weight
:
600
;
line-height
:
1.45
;
text-align
:
left
;
cursor
:
pointer
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.task-panel-
artifac
t-url
:hover
,
.task-panel-
artifac
t-url
:focus-visible
{
.task-panel-
outpu
t-url
:hover
,
.task-panel-
outpu
t-url
:focus-visible
{
color
:
#1d4ed8
;
text-decoration
:
underline
;
text-underline-offset
:
3px
;
}
.task-panel-
artifac
t-url
:focus-visible
{
.task-panel-
outpu
t-url
:focus-visible
{
outline
:
2px
solid
rgba
(
37
,
99
,
235
,
0.34
);
outline-offset
:
2px
;
border-radius
:
6px
;
}
.task-panel-output-meta
{
display
:
grid
;
grid-template-columns
:
32px
minmax
(
0
,
1
fr
);
gap
:
8px
;
align-items
:
center
;
}
.task-panel-output-meta
>
div
{
display
:
grid
;
gap
:
2px
;
}
.task-panel-output-meta
>
div
strong
,
.task-panel-output-meta
>
div
span
{
min-width
:
0
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.task-panel-output-meta
>
div
strong
{
color
:
#1c324d
;
font-size
:
12.5px
;
font-weight
:
700
;
line-height
:
1.35
;
}
.task-panel-output-meta
>
div
span
{
color
:
#64748b
;
font-size
:
12px
;
font-weight
:
600
;
line-height
:
1.4
;
}
.task-panel-output-empty
{
min-height
:
180px
;
}
.task-panel-artifact-copied
{
position
:
static
;
flex
:
0
0
auto
;
padding
:
5px
9px
;
border-radius
:
999px
;
border
:
1px
solid
rgba
(
167
,
243
,
208
,
0.9
);
background
:
rgba
(
240
,
253
,
250
,
0.96
);
color
:
#047857
;
font-size
:
12px
;
font-weight
:
650
;
line-height
:
1.5
;
text-align
:
right
;
line-height
:
1
;
box-shadow
:
0
8px
20px
rgba
(
15
,
23
,
42
,
0.12
);
pointer-events
:
none
;
white-space
:
nowrap
;
}
.task-panel-loading-state
{
display
:
grid
;
gap
:
6px
;
min-width
:
860px
;
min-width
:
0
;
}
.task-panel-loading-row
{
display
:
grid
;
grid-template-columns
:
3
2px
106px
150px
138px
minmax
(
280px
,
1
fr
);
grid-template-columns
:
3
8px
repeat
(
4
,
minmax
(
0
,
1
fr
)
);
gap
:
12px
;
align-items
:
center
;
min-height
:
82px
;
...
...
@@ -657,8 +744,16 @@
}
@media
(
max-width
:
980px
)
{
.task-panel-scroll
{
overflow-x
:
auto
;
.task-panel-stats
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
.task-panel-output-item
{
grid-template-columns
:
30px
minmax
(
0
,
1
fr
);
}
.task-panel-output-meta
{
grid-column
:
2
;
}
}
...
...
@@ -677,33 +772,30 @@
width
:
fit-content
;
}
.task-panel-table
,
.task-panel-loading-state
{
min-width
:
0
;
.task-panel-stats
{
grid-template-columns
:
minmax
(
0
,
1
fr
);
}
.task-panel-row
{
grid-template-columns
:
minmax
(
0
,
1
fr
);
gap
:
10px
;
min-height
:
0
;
.task-panel-stat-card
{
min-height
:
78px
;
}
.task-panel-
row-head
{
display
:
none
;
.task-panel-
output-item
{
grid-template-columns
:
minmax
(
0
,
1
fr
)
;
}
.task-panel-row
:not
(
.task-panel-row-head
)
>
div
:nth-child
(
3
)
{
padding-left
:
0
;
.task-panel-output-icon
,
.task-panel-output-meta
{
grid-column
:
1
;
}
.task-panel-loading-row
{
grid-template-columns
:
32px
minmax
(
0
,
1
fr
);
.task-panel-output-icon
{
width
:
30px
;
height
:
30px
;
}
.task-panel-loading-line-task
,
.task-panel-loading-pill
,
.task-panel-loading-line-artifact
{
grid-column
:
1
/
-1
;
.task-panel-loading-row
{
grid-template-columns
:
38px
minmax
(
0
,
1
fr
);
}
}
...
...
@@ -716,12 +808,12 @@
animation
:
none
;
}
.task-panel-
row
{
.task-panel-
output-item
{
transition
:
none
;
}
.task-panel-
row
:not
(
.task-panel-row-head
)
:hover
,
.task-panel-
row
:not
(
.task-panel-row-head
)
:focus-within
{
.task-panel-
output-item
:hover
,
.task-panel-
output-item
:focus-within
{
transform
:
none
;
}
}
packages/shared-types/src/index.ts
View file @
8645a0f7
...
...
@@ -861,6 +861,7 @@ export interface TaskPanelArtifact {
id
:
string
;
name
:
string
;
kind
?:
string
;
summary
?:
string
;
url
?:
string
;
}
...
...
@@ -871,6 +872,9 @@ export interface TaskPanelItem {
taskTitle
:
string
;
status
:
TaskPanelStatus
;
statusDetail
:
string
;
creditsUsed
?:
number
;
messageCount
?:
number
;
updatedAt
?:
string
;
artifacts
:
TaskPanelArtifact
[];
}
...
...
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