mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-21 21:47:00 +08:00
feat: 优化全局 UI 交互与页面视觉体验
统一按钮、输入框、选择控件和开关的交互状态 优化日报、导出、设置、Agent Hub 和 API Center 的UI 完善组件、单元及 Electron E2E 测试
This commit is contained in:
@@ -24,3 +24,4 @@ findings.md
|
||||
progress.md
|
||||
task_plan.md
|
||||
.agents
|
||||
*__screenshots__
|
||||
@@ -69,7 +69,7 @@ interface ExportConfigurationPanelProps {
|
||||
const sectionClassName = 'mb-6'
|
||||
const sectionTitleClassName = 'mb-3 text-xs font-bold tracking-normal text-foreground'
|
||||
const helperClassName = 'mb-0 mt-2 text-[11px] leading-[17px] text-muted-foreground'
|
||||
const choicePanelClassName = 'rounded-lg bg-muted p-3'
|
||||
const choicePanelClassName = 'rounded-md bg-muted/70 p-3'
|
||||
|
||||
export function ExportConfigurationPanel({
|
||||
taskCenter,
|
||||
@@ -322,7 +322,7 @@ export function ExportConfigurationPanel({
|
||||
|
||||
<section className={sectionClassName}>
|
||||
<h3 className={sectionTitleClassName}>资源处理</h3>
|
||||
<label className="flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-border bg-surface px-3.5 py-3 text-xs text-foreground">
|
||||
<label className="flex cursor-pointer items-center justify-between gap-3 border-b border-border-subtle py-2.5 text-xs text-foreground">
|
||||
<span>包含图片、视频、语音、表情及文件附件</span>
|
||||
<Checkbox
|
||||
checked={includeMedia}
|
||||
@@ -387,7 +387,7 @@ export function ExportConfigurationPanel({
|
||||
))}
|
||||
</div>
|
||||
<p className={helperClassName}>媒体资源会延长导出时间,缺失资源不会中断任务。</p>
|
||||
<label className="mt-2 flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-border bg-surface px-3.5 py-3 text-xs text-foreground">
|
||||
<label className="mt-2 flex cursor-pointer items-center justify-between gap-3 border-b border-border-subtle py-2.5 text-xs text-foreground">
|
||||
<span>在聊天气泡旁显示头像</span>
|
||||
<Checkbox
|
||||
checked={includeAvatars}
|
||||
@@ -424,7 +424,7 @@ export function ExportConfigurationPanel({
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer className="flex h-[58px] shrink-0 items-center gap-2 border-t border-border bg-surface px-5.5 text-xs text-muted-foreground">
|
||||
<footer className="flex px-4 h-[58px] shrink-0 items-center gap-2 border-t border-border bg-surface px-5.5 text-xs text-muted-foreground">
|
||||
<span
|
||||
className={`h-2 w-2 shrink-0 rounded-full ${status === 'completed' ? 'bg-primary' : 'bg-success'}`}
|
||||
aria-hidden
|
||||
|
||||
@@ -40,7 +40,7 @@ export function ModelSummary({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="report-config-section">
|
||||
<section className="report-config-section report-model-config-section">
|
||||
<div className="report-model-summary">
|
||||
<div className="report-model-summary-content">
|
||||
<h3>模型配置</h3>
|
||||
|
||||
@@ -86,7 +86,7 @@ export function ReportToolbar({
|
||||
<Button variant="outline" size="sm" onClick={onRegenerate}>
|
||||
重新生成
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={!canCopyImage} onClick={onCopyImage}>
|
||||
<Button variant="ghost" size="sm" disabled={!canCopyImage} onClick={onCopyImage}>
|
||||
复制图片
|
||||
</Button>
|
||||
<span
|
||||
|
||||
@@ -359,12 +359,10 @@ export function AISearchWorkspace({
|
||||
|
||||
const renderIdle = (): React.ReactElement => (
|
||||
<div className="ai-search-empty">
|
||||
<div className="ai-search-empty-mark" aria-hidden>
|
||||
<span>✦</span>
|
||||
</div>
|
||||
<span className="ai-search-kicker">LOCAL AI WORKSPACE</span>
|
||||
<span className="ai-search-kicker">本地搜索</span>
|
||||
<h2>把聊天记录变成可追问的答案</h2>
|
||||
<p>聊天数据在本机检索并保留证据;使用外部 AI 服务前会说明并请求确认发送范围。</p>
|
||||
<span className="ai-search-prompt-label">可以这样问</span>
|
||||
<div className="ai-search-prompts">
|
||||
{[
|
||||
'交友群"张三"最近聊了什么?',
|
||||
@@ -372,9 +370,16 @@ export function AISearchWorkspace({
|
||||
'我和"老李"最近聊了什么话题?',
|
||||
'全局搜一下 我和谁聊过 去健身?'
|
||||
].map((prompt) => (
|
||||
<button key={prompt} type="button" onClick={() => setQuery(prompt)}>
|
||||
<Button
|
||||
key={prompt}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto w-full justify-start rounded-none px-0 py-2.5 text-left font-normal"
|
||||
onClick={() => setQuery(prompt)}
|
||||
>
|
||||
{prompt}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -781,7 +786,7 @@ export function AISearchWorkspace({
|
||||
<div className="ai-search-workspace">
|
||||
<header className="ai-search-header">
|
||||
<div>
|
||||
<span className="ai-search-kicker">TraceMemo · LOCAL INTELLIGENCE</span>
|
||||
<span className="ai-search-kicker">TraceMemo 本地搜索</span>
|
||||
<h1>问问你的微信</h1>
|
||||
<p>在本地聊天记录中提炼主题、结论和可追溯证据</p>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '../../lib/cn'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border-0 bg-transparent text-sm font-medium transition-[color,background-color,border-color,transform] duration-fast ease-tm-standard active:translate-y-px disabled:pointer-events-none disabled:translate-y-0 disabled:!text-disabled-foreground disabled:opacity-100',
|
||||
'inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border-0 bg-transparent text-sm font-medium transition-[color,background-color,border-color,transform] duration-fast ease-tm-standard focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background active:translate-y-px disabled:pointer-events-none disabled:translate-y-0 disabled:!text-disabled-foreground disabled:opacity-100',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -9,7 +9,7 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-muted-foreground/60 bg-surface transition-colors duration-fast ease-tm-standard hover:border-primary/70 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground disabled:data-[state=checked]:border-disabled-border disabled:data-[state=checked]:bg-primary/40',
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-muted-foreground/60 bg-surface transition-colors duration-fast ease-tm-standard hover:border-primary/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground disabled:data-[state=checked]:border-disabled-border disabled:data-[state=checked]:bg-primary/40',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -47,7 +47,7 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-md p-1 text-muted-foreground opacity-70 transition-opacity hover:bg-accent hover:text-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none">
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 inline-flex h-8 w-8 items-center justify-center rounded-md border-0 bg-transparent p-0 text-muted-foreground opacity-70 shadow-none transition-colors duration-fast hover:bg-accent hover:text-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none">
|
||||
<span aria-hidden="true" className="text-lg leading-none">
|
||||
×
|
||||
</span>
|
||||
|
||||
@@ -15,7 +15,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-[state=open]:bg-accent',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
@@ -67,7 +67,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-[disabled]:pointer-events-none data-[disabled]:text-disabled-foreground data-[disabled]:opacity-100',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
@@ -84,7 +84,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
ref={ref}
|
||||
checked={checked}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-[disabled]:pointer-events-none data-[disabled]:text-disabled-foreground data-[disabled]:opacity-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -104,7 +104,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-[disabled]:pointer-events-none data-[disabled]:text-disabled-foreground data-[disabled]:opacity-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function EmptyState({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-h-40 flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-surface-muted/50 px-6 py-8 text-center',
|
||||
'flex min-h-40 flex-col items-center justify-center gap-3 rounded-md border border-dashed border-border-subtle bg-surface-muted/40 px-6 py-8 text-center',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ export const Input = React.forwardRef<
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-control-form w-full rounded-md border border-border bg-surface px-3 py-1 text-sm text-foreground transition-colors duration-fast ease-tm-standard placeholder:text-muted-foreground hover:border-primary/25 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 disabled:placeholder:text-disabled-foreground',
|
||||
'flex h-control-form w-full rounded-md border border-border-subtle bg-surface px-3 py-1 text-sm text-foreground transition-colors duration-fast ease-tm-standard placeholder:text-muted-foreground hover:border-primary/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 disabled:placeholder:text-disabled-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -17,7 +17,7 @@ const RadioGroupItem = React.forwardRef<
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'aspect-square h-4 w-4 rounded-full border border-muted-foreground/60 bg-surface text-primary transition-colors duration-fast ease-tm-standard hover:border-primary/70 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:border-primary disabled:data-[state=checked]:border-disabled-border',
|
||||
'aspect-square h-4 w-4 rounded-full border border-muted-foreground/60 bg-surface text-primary transition-colors duration-fast ease-tm-standard hover:border-primary/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:border-primary disabled:data-[state=checked]:border-disabled-border',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -21,7 +21,7 @@ const SegmentedControlItem = React.forwardRef<
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-8 min-w-0 items-center justify-center rounded-sm border-0 bg-transparent px-3 text-xs font-medium text-muted-foreground transition-[color,background-color,font-weight] duration-fast ease-tm-standard hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:bg-accent data-[state=checked]:font-semibold data-[state=checked]:text-accent-foreground',
|
||||
'inline-flex h-8 min-w-0 items-center justify-center rounded-sm border-0 bg-transparent px-3 text-xs font-medium text-muted-foreground transition-[color,background-color,font-weight,transform] duration-fast ease-tm-standard hover:bg-accent hover:text-foreground active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:bg-accent data-[state=checked]:font-semibold data-[state=checked]:text-accent-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -13,7 +13,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-control-form w-full items-center justify-between rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground transition-colors duration-fast ease-tm-standard hover:border-primary/25 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 [&>span]:line-clamp-1',
|
||||
'flex h-control-form w-full items-center justify-between rounded-md border border-border-subtle bg-surface px-3 py-2 text-sm text-foreground transition-colors duration-fast ease-tm-standard hover:border-primary/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 [&>span]:line-clamp-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -9,12 +9,12 @@ const Switch = React.forwardRef<
|
||||
<SwitchPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-muted transition-colors duration-fast ease-tm-standard hover:bg-muted/80 disabled:cursor-not-allowed disabled:bg-disabled-surface disabled:opacity-100 data-[state=checked]:bg-primary disabled:data-[state=checked]:bg-primary/40',
|
||||
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-0 bg-muted-foreground/25 p-0.5 transition-colors duration-fast ease-tm-standard hover:bg-muted-foreground/35 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:bg-disabled-border disabled:opacity-100 data-[state=checked]:bg-primary data-[state=checked]:hover:bg-primary-hover disabled:data-[state=checked]:bg-primary/40',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb className="pointer-events-none block h-4 w-4 rounded-full bg-white shadow-surface ring-0 transition-transform duration-fast ease-tm-standard data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" />
|
||||
<SwitchPrimitive.Thumb className="pointer-events-none block h-4 w-4 rounded-full bg-background shadow-surface ring-0 transition-transform duration-fast ease-tm-standard data-[state=checked]:translate-x-4 data-[state=checked]:bg-primary-foreground data-[state=unchecked]:translate-x-0" />
|
||||
</SwitchPrimitive.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitive.Root.displayName
|
||||
|
||||
@@ -26,7 +26,7 @@ const TabsTrigger = React.forwardRef<
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-sm border-0 bg-transparent px-3 py-1.5 text-xs font-medium text-muted-foreground transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-surface data-[state=active]:text-foreground data-[state=active]:shadow-surface',
|
||||
'inline-flex items-center justify-center rounded-sm border-0 bg-transparent px-3 py-1.5 text-xs font-medium text-muted-foreground transition-[color,background-color,box-shadow,transform] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:text-disabled-foreground disabled:opacity-100 active:scale-[0.98] data-[state=active]:bg-surface data-[state=active]:text-foreground data-[state=active]:shadow-surface',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const Textarea = React.forwardRef<
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex min-h-20 w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground shadow-surface transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex min-h-20 w-full rounded-md border border-border-subtle bg-surface px-3 py-2 text-sm text-foreground transition-colors duration-fast ease-tm-standard placeholder:text-muted-foreground hover:border-primary/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 disabled:placeholder:text-disabled-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -64,12 +64,12 @@ const ToastClose = React.forwardRef<
|
||||
<ToastPrimitive.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring group-hover:opacity-100',
|
||||
'inline-flex h-7 w-7 shrink-0 appearance-none items-center justify-center rounded-md border-0 bg-transparent p-0 text-lg leading-none text-muted-foreground opacity-70 transition-colors duration-fast hover:bg-accent hover:text-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-surface group-hover:opacity-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
×
|
||||
<span aria-hidden>×</span>
|
||||
</ToastPrimitive.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitive.Close.displayName
|
||||
|
||||
@@ -194,37 +194,19 @@ export function AgentHubWorkspace(): React.ReactElement {
|
||||
</section>
|
||||
|
||||
<aside className="agent-hub-card agent-hub-capability-card">
|
||||
<span className="agent-hub-card-kicker">已启用能力</span>
|
||||
<h2>微信数据助手</h2>
|
||||
<span className="agent-hub-card-kicker">本机能力</span>
|
||||
<h2>能力与状态</h2>
|
||||
<p>机器人通过本机 Agent Hub 调用 TraceMemo,不向公网暴露数据库。</p>
|
||||
<div className="agent-hub-example">
|
||||
<span>支持自然语言,可以这样问</span>
|
||||
<strong>“最近 5 条消息是谁?”</strong>
|
||||
<strong>“帮我看看最近跟xx聊了些什么”</strong>
|
||||
<strong>“生成产品交流群今天的群聊总结图片”</strong>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<i />
|
||||
本机 HTTP 通信
|
||||
</li>
|
||||
<li>
|
||||
<i />
|
||||
入站请求鉴权
|
||||
</li>
|
||||
<li>
|
||||
<i />
|
||||
消息重复保护
|
||||
</li>
|
||||
<li>
|
||||
<i />
|
||||
使用已配置 AI 理解自然语言
|
||||
</li>
|
||||
<li>
|
||||
<li>本机 HTTP 通信</li>
|
||||
<li>入站请求鉴权</li>
|
||||
<li>消息重复保护</li>
|
||||
<li>使用已配置 AI 理解自然语言</li>
|
||||
<li className="agent-hub-capability-status">
|
||||
<i className={status.dataApi === 'online' ? '' : 'offline'} />
|
||||
本地数据 API:{status.dataApi === 'online' ? '已连接' : '未连接'}
|
||||
</li>
|
||||
<li>
|
||||
<li className="agent-hub-capability-status">
|
||||
<i className={status.databaseReady ? '' : 'offline'} />
|
||||
微信数据库:{status.databaseReady ? '可查询' : '未就绪'}
|
||||
</li>
|
||||
|
||||
@@ -87,13 +87,13 @@ export function ApiRuntimePanel({
|
||||
启动服务
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" variant="outline" onClick={() => onControl('restart')}>
|
||||
<Button size="sm" variant="ghost" onClick={() => onControl('restart')}>
|
||||
重启服务
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={copyAddress}>
|
||||
<Button size="sm" variant="ghost" onClick={copyAddress}>
|
||||
复制地址
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={onOpenSettings}>
|
||||
<Button size="sm" variant="ghost" onClick={onOpenSettings}>
|
||||
API 设置
|
||||
</Button>
|
||||
</div>
|
||||
@@ -117,7 +117,7 @@ export function ApiRuntimePanel({
|
||||
<div className="api-runtime-actions">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
disabled={!tokenStatus?.hasToken}
|
||||
onClick={() => void (revealedToken ? onHideToken() : onRevealToken())}
|
||||
>
|
||||
@@ -125,7 +125,7 @@ export function ApiRuntimePanel({
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
disabled={!tokenStatus?.hasToken}
|
||||
onClick={() => void onCopyToken()}
|
||||
>
|
||||
@@ -162,7 +162,7 @@ export function ApiRuntimePanel({
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
onClick={() => void onCopy(formatJson(response.text), 'JSON 响应已复制')}
|
||||
>
|
||||
复制 JSON
|
||||
|
||||
@@ -47,7 +47,7 @@ export function SkillDetails({
|
||||
<Button size="sm" variant="outline" onClick={onPreview} disabled={!skill?.available}>
|
||||
预览 SKILL.md
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={onGithub}>
|
||||
<Button size="sm" variant="ghost" onClick={onGithub}>
|
||||
查看 GitHub 最新版本
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -100,6 +100,13 @@ export function SettingsWorkspace({
|
||||
return <AdvancedPage onNotice={onNotice} />
|
||||
case 'cache-cleanup':
|
||||
return <CacheCleanupPage onNotice={onNotice} />
|
||||
case 'storage-export':
|
||||
return (
|
||||
<SettingsEmptyState
|
||||
label={SETTINGS_CATEGORY_LABELS[selectedCategory]}
|
||||
description="导出格式与范围请前往「导出」工作区设置。"
|
||||
/>
|
||||
)
|
||||
case 'appearance':
|
||||
return <AppearancePage onNotice={onNotice} onAppearanceChange={onAppearanceChange} />
|
||||
case 'about':
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
export function SettingsEmptyState({ label }: { label: string }): React.ReactElement {
|
||||
export function SettingsEmptyState({
|
||||
label,
|
||||
description = '当前分类暂无额外设置。'
|
||||
}: {
|
||||
label: string
|
||||
description?: string
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className="settings-empty-state">
|
||||
<h2>{label}</h2>
|
||||
<p>该设置将在后续阶段接入。</p>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ export const SETTINGS_NAVIGATION: SettingsNavigationGroup[] = [
|
||||
{
|
||||
label: '数据管理',
|
||||
items: [
|
||||
{ id: 'storage-export', label: '存储与导出' },
|
||||
// “存储与导出”暂不开放;保留 category/render case 以兼容已有页面状态。
|
||||
// { id: 'storage-export', label: '存储与导出' },
|
||||
{ id: 'cache-cleanup', label: '缓存与清理' }
|
||||
]
|
||||
},
|
||||
@@ -40,6 +41,9 @@ export const SETTINGS_NAVIGATION: SettingsNavigationGroup[] = [
|
||||
}
|
||||
]
|
||||
|
||||
export const SETTINGS_CATEGORY_LABELS = Object.fromEntries(
|
||||
SETTINGS_NAVIGATION.flatMap((group) => group.items.map((item) => [item.id, item.label]))
|
||||
) as Record<SettingsCategoryId, string>
|
||||
export const SETTINGS_CATEGORY_LABELS = {
|
||||
...Object.fromEntries(
|
||||
SETTINGS_NAVIGATION.flatMap((group) => group.items.map((item) => [item.id, item.label]))
|
||||
),
|
||||
'storage-export': '存储与导出'
|
||||
} as Record<SettingsCategoryId, string>
|
||||
|
||||
@@ -14,25 +14,30 @@ export function AIModelPage({
|
||||
onNotice: (message: string) => void
|
||||
}): React.ReactElement {
|
||||
const controller = useAIModelSettingsController({ onRuntimeChange, onNotice })
|
||||
const shouldRevealNewEditor = useRef(false)
|
||||
const shouldRevealEditor = useRef(false)
|
||||
const runtime = controller.state.runtime
|
||||
const defaultProvider = controller.state.providers.find(
|
||||
(provider) => provider.id === runtime?.providerId
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!controller.state.editor || !shouldRevealNewEditor.current) return
|
||||
shouldRevealNewEditor.current = false
|
||||
if (!controller.state.editor || !shouldRevealEditor.current) return
|
||||
shouldRevealEditor.current = false
|
||||
const editor = document.getElementById('ai-provider-editor')
|
||||
editor?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
editor?.querySelector<HTMLInputElement>('#ai-provider-name')?.focus({ preventScroll: true })
|
||||
}, [controller.state.editor])
|
||||
|
||||
const openNewProvider = (): void => {
|
||||
shouldRevealNewEditor.current = true
|
||||
shouldRevealEditor.current = true
|
||||
controller.openNew()
|
||||
}
|
||||
|
||||
const openProviderEditor = (provider: (typeof controller.state.providers)[number]): void => {
|
||||
shouldRevealEditor.current = true
|
||||
controller.openEdit(provider)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-page ai-model-page">
|
||||
<header className="settings-page-header">
|
||||
@@ -85,7 +90,7 @@ export function AIModelPage({
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
testing={controller.state.testingId === provider.id}
|
||||
onEdit={() => controller.openEdit(provider)}
|
||||
onEdit={() => openProviderEditor(provider)}
|
||||
onTest={() => void controller.test(provider.id)}
|
||||
onDefault={() => void controller.setDefault(provider.id)}
|
||||
onDelete={() => void controller.remove(provider.id)}
|
||||
|
||||
@@ -511,7 +511,11 @@ export function VoiceRecognitionPage({
|
||||
onCheckedChange={() => toggleConversation(contact.md5)}
|
||||
/>
|
||||
<span className="voice-conversation-avatar" aria-hidden>
|
||||
{contactName(contact).slice(0, 1)}
|
||||
{contact.avatar ? (
|
||||
<img src={contact.avatar} alt="" referrerPolicy="no-referrer" />
|
||||
) : (
|
||||
contactName(contact).slice(0, 1)
|
||||
)}
|
||||
</span>
|
||||
<span className="voice-conversation-copy">
|
||||
<strong>{contactName(contact)}</strong>
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-bg-sidebar);
|
||||
color: var(--wxex-text-secondary);
|
||||
@@ -187,7 +187,7 @@
|
||||
.api-introduction {
|
||||
margin-top: 24px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-lg);
|
||||
background: var(--wxex-bg-elevated);
|
||||
}
|
||||
@@ -202,7 +202,7 @@
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px dashed var(--wxex-border);
|
||||
border: 1px dashed hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-md);
|
||||
color: var(--wxex-text-secondary);
|
||||
font: 12px/18px var(--wxex-font);
|
||||
@@ -210,7 +210,7 @@
|
||||
.api-flow span,
|
||||
.api-flow strong {
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: 5px;
|
||||
background: var(--wxex-bg-elevated);
|
||||
white-space: nowrap;
|
||||
@@ -230,7 +230,7 @@
|
||||
.skill-install-flow {
|
||||
margin-top: 26px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-lg);
|
||||
background: var(--wxex-bg-elevated);
|
||||
}
|
||||
@@ -262,7 +262,7 @@
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
place-items: center;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: 50%;
|
||||
background: var(--wxex-bg-elevated);
|
||||
font: 700 12px/1 var(--wxex-font);
|
||||
@@ -291,7 +291,7 @@
|
||||
}
|
||||
.api-skill-details {
|
||||
margin-top: 16px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-bg-elevated);
|
||||
}
|
||||
@@ -331,7 +331,7 @@
|
||||
.api-endpoint-table {
|
||||
margin-top: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-bg-elevated);
|
||||
}
|
||||
@@ -350,7 +350,7 @@
|
||||
}
|
||||
.api-endpoint-row {
|
||||
min-height: 54px;
|
||||
border-top: 1px solid var(--wxex-border);
|
||||
border-top: 1px solid hsl(var(--tm-border-subtle));
|
||||
color: var(--wxex-text-secondary);
|
||||
font: 12px/18px var(--wxex-font);
|
||||
}
|
||||
@@ -387,7 +387,7 @@
|
||||
gap: 10px;
|
||||
margin: 12px 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-bg-sidebar);
|
||||
overflow: hidden;
|
||||
@@ -505,7 +505,7 @@
|
||||
overflow-wrap: anywhere;
|
||||
margin: 10px 0;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-sm);
|
||||
background: var(--wxex-bg-subtle);
|
||||
color: var(--wxex-text-primary);
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
background: var(--wxex-bg-main);
|
||||
}
|
||||
|
||||
.export-config-panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-shadow:
|
||||
-10px 0 18px -16px color-mix(in srgb, var(--wxex-text-primary) 32%, transparent),
|
||||
10px 0 18px -16px color-mix(in srgb, var(--wxex-text-primary) 32%, transparent);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.export-workspace {
|
||||
grid-template-columns: 248px minmax(460px, 1fr);
|
||||
|
||||
@@ -262,6 +262,10 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.report-model-config-section {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.report-empty-state h2,
|
||||
.report-privacy-note h3,
|
||||
.report-config-section h3,
|
||||
@@ -342,11 +346,11 @@
|
||||
.report-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-border);
|
||||
background: var(--wxex-bg-elevated);
|
||||
}
|
||||
|
||||
.report-check-row {
|
||||
@@ -357,12 +361,22 @@
|
||||
min-width: 0;
|
||||
padding: 11px 12px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--wxex-border);
|
||||
border-radius: 0;
|
||||
background: var(--wxex-bg-elevated);
|
||||
color: var(--wxex-text-primary);
|
||||
transition: background-color var(--tm-motion-fast) var(--tm-ease-standard);
|
||||
}
|
||||
|
||||
.report-check-row:nth-child(odd):not(:last-child) {
|
||||
border-right: 1px solid var(--wxex-border);
|
||||
}
|
||||
|
||||
.report-check-row:last-child {
|
||||
grid-column: 1 / -1;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.report-check-row:hover {
|
||||
background: var(--wxex-bg-sidebar);
|
||||
}
|
||||
@@ -930,7 +944,7 @@
|
||||
.agent-hub-card {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-lg);
|
||||
background: var(--wxex-bg-elevated);
|
||||
}
|
||||
@@ -950,7 +964,7 @@
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--wxex-border);
|
||||
border-bottom: 1px solid hsl(var(--tm-border-subtle));
|
||||
}
|
||||
.agent-hub-card h2 {
|
||||
margin: 4px 0 0;
|
||||
@@ -1110,39 +1124,21 @@
|
||||
.agent-hub-capability-card > p {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.agent-hub-example {
|
||||
margin: 18px 0 14px;
|
||||
padding: 14px;
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-brand-soft);
|
||||
}
|
||||
.agent-hub-example span {
|
||||
display: block;
|
||||
color: var(--wxex-text-muted);
|
||||
font: 600 11px/16px var(--wxex-font);
|
||||
}
|
||||
.agent-hub-example strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--wxex-brand);
|
||||
font: 700 14px/21px var(--wxex-font);
|
||||
}
|
||||
.agent-hub-capability-card ul {
|
||||
margin: 0;
|
||||
margin: 18px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.agent-hub-capability-card li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
gap: 8px;
|
||||
padding: 7px 0;
|
||||
border-bottom: 1px solid var(--wxex-border);
|
||||
color: var(--wxex-text-secondary);
|
||||
font: 13px/19px var(--wxex-font);
|
||||
}
|
||||
.agent-hub-capability-card li:last-child {
|
||||
border-bottom: 0;
|
||||
.agent-hub-capability-card li + li {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.agent-hub-capability-card li i {
|
||||
width: 7px;
|
||||
@@ -1150,10 +1146,8 @@
|
||||
border-radius: 50%;
|
||||
background: var(--wxex-brand);
|
||||
}
|
||||
.agent-hub-example strong + strong {
|
||||
margin-top: 9px;
|
||||
padding-top: 9px;
|
||||
border-top: 1px solid rgba(36, 122, 99, 0.15);
|
||||
.agent-hub-capability-card li:not(.agent-hub-capability-status) {
|
||||
padding-left: 15px;
|
||||
}
|
||||
.agent-hub-capability-card li i.offline {
|
||||
background: var(--wxex-danger);
|
||||
|
||||
@@ -224,29 +224,6 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.report-viewer-toolbar button {
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-bg-elevated);
|
||||
color: var(--wxex-text-primary);
|
||||
cursor: pointer;
|
||||
font: 600 13px/18px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-viewer-toolbar button.primary {
|
||||
border-color: var(--wxex-ai);
|
||||
background: var(--wxex-ai);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.report-viewer-toolbar button:disabled {
|
||||
color: var(--wxex-text-muted);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.report-toolbar-button-hint {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
gap: 10px;
|
||||
margin-top: auto;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-bg-elevated);
|
||||
}
|
||||
@@ -388,39 +388,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ai-search-empty-mark {
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--wxex-brand-soft);
|
||||
color: var(--wxex-ai);
|
||||
font-size: 28px;
|
||||
.ai-search-empty {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
|
||||
> h2,
|
||||
> p {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-search-prompt-label {
|
||||
margin-top: 28px;
|
||||
color: var(--wxex-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.ai-search-prompts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
margin-top: 22px;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
display: grid;
|
||||
margin-top: 6px;
|
||||
border-top: 1px solid hsl(var(--tm-border-subtle));
|
||||
|
||||
button {
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border-radius: 999px;
|
||||
background: var(--wxex-bg-elevated);
|
||||
color: var(--wxex-text-secondary);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--wxex-brand);
|
||||
color: var(--wxex-brand);
|
||||
}
|
||||
> * {
|
||||
border-bottom: 1px solid hsl(var(--tm-border-subtle));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,18 +557,6 @@
|
||||
width: min(820px, calc(100% - 48px));
|
||||
margin: 0 auto;
|
||||
padding: 28px 0 34px;
|
||||
animation: ai-search-result-in 260ms ease-out both;
|
||||
}
|
||||
|
||||
@keyframes ai-search-result-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-search-result-header {
|
||||
@@ -610,41 +593,6 @@
|
||||
|
||||
.ai-search-summary-block {
|
||||
margin-top: 22px;
|
||||
animation: ai-search-result-in 340ms 45ms ease-out both;
|
||||
}
|
||||
|
||||
.ai-search-answer > * {
|
||||
animation: ai-search-answer-line-in 260ms ease-out both;
|
||||
}
|
||||
|
||||
.ai-search-answer > *:nth-child(1) {
|
||||
animation-delay: 70ms;
|
||||
}
|
||||
.ai-search-answer > *:nth-child(2) {
|
||||
animation-delay: 105ms;
|
||||
}
|
||||
.ai-search-answer > *:nth-child(3) {
|
||||
animation-delay: 140ms;
|
||||
}
|
||||
.ai-search-answer > *:nth-child(4) {
|
||||
animation-delay: 175ms;
|
||||
}
|
||||
.ai-search-answer > *:nth-child(5) {
|
||||
animation-delay: 210ms;
|
||||
}
|
||||
.ai-search-answer > *:nth-child(n + 6) {
|
||||
animation-delay: 245ms;
|
||||
}
|
||||
|
||||
@keyframes ai-search-answer-line-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-search-section-heading {
|
||||
@@ -688,7 +636,7 @@
|
||||
|
||||
.ai-search-answer-evidence {
|
||||
padding: 9px 12px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-top: 0;
|
||||
background: var(--wxex-bg-elevated);
|
||||
}
|
||||
|
||||
@@ -283,6 +283,12 @@
|
||||
background: var(--wxex-bg-sidebar);
|
||||
color: var(--wxex-text-secondary);
|
||||
font-size: 12px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.voice-conversation-copy {
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
.settings-card {
|
||||
border: 1px solid var(--wxex-border);
|
||||
border: 1px solid hsl(var(--tm-border-subtle));
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-bg-elevated);
|
||||
padding: 24px 26px;
|
||||
|
||||
@@ -74,6 +74,23 @@ describe('AI model settings', () => {
|
||||
expect(screen.getByLabelText('模型 ID')).toHaveAttribute('placeholder', '例如:deepseek-chat')
|
||||
})
|
||||
|
||||
it('reveals and focuses the provider editor when editing an existing provider', async () => {
|
||||
vi.mocked(window.api.listAIProviders).mockResolvedValue({
|
||||
success: true,
|
||||
providers: [provider]
|
||||
})
|
||||
render(<AIModelPage onRuntimeChange={vi.fn()} onNotice={vi.fn()} />)
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve()
|
||||
})
|
||||
await userEvent.click(screen.getByRole('button', { name: '编辑' }))
|
||||
|
||||
expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start' })
|
||||
expect(screen.getByLabelText('供应商名称')).toHaveFocus()
|
||||
expect(screen.getByLabelText('供应商名称')).toHaveValue(provider.name)
|
||||
})
|
||||
|
||||
it('does not reload providers when the parent callback identity changes', async () => {
|
||||
const firstRuntimeChange = vi.fn()
|
||||
const secondRuntimeChange = vi.fn()
|
||||
|
||||
@@ -11,8 +11,8 @@ describe('empty states', () => {
|
||||
})
|
||||
|
||||
it('labels an unavailable settings section explicitly', () => {
|
||||
render(<SettingsEmptyState label="测试设置" />)
|
||||
expect(screen.getByRole('heading', { name: '测试设置' })).toBeVisible()
|
||||
expect(screen.getByText('该设置将在后续阶段接入。')).toBeVisible()
|
||||
render(<SettingsEmptyState label="存储与导出" description="请前往导出工作区设置。" />)
|
||||
expect(screen.getByRole('heading', { name: '存储与导出' })).toBeVisible()
|
||||
expect(screen.getByText('请前往导出工作区设置。')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Switch
|
||||
Switch,
|
||||
Textarea
|
||||
} from '../../src/renderer/src/components/ui'
|
||||
import { IconButton } from '../../src/renderer/src/components/ui/icon-button'
|
||||
import {
|
||||
@@ -79,6 +80,7 @@ describe('TraceMemo UI control states', () => {
|
||||
<>
|
||||
<Button disabled>不可用操作</Button>
|
||||
<Input aria-label="不可用输入" value="仍可读取" disabled readOnly />
|
||||
<Textarea aria-label="不可用多行输入" value="仍可读取" disabled readOnly />
|
||||
<Checkbox aria-label="不可用多选" disabled />
|
||||
<RadioGroup aria-label="不可用单选组">
|
||||
<RadioGroupItem value="disabled" aria-label="不可用单选" disabled />
|
||||
@@ -90,6 +92,7 @@ describe('TraceMemo UI control states', () => {
|
||||
for (const control of [
|
||||
screen.getByRole('button', { name: '不可用操作' }),
|
||||
screen.getByRole('textbox', { name: '不可用输入' }),
|
||||
screen.getByRole('textbox', { name: '不可用多行输入' }),
|
||||
screen.getByRole('checkbox', { name: '不可用多选' }),
|
||||
screen.getByRole('radio', { name: '不可用单选' }),
|
||||
screen.getByRole('switch', { name: '不可用开关' })
|
||||
@@ -103,6 +106,15 @@ describe('TraceMemo UI control states', () => {
|
||||
'disabled:text-disabled-foreground',
|
||||
'disabled:!text-disabled-foreground'
|
||||
)
|
||||
expect(screen.getByRole('textbox', { name: '不可用多行输入' })).toHaveClass(
|
||||
'border-border-subtle',
|
||||
'disabled:border-disabled-border',
|
||||
'disabled:bg-disabled-surface',
|
||||
'disabled:text-disabled-foreground'
|
||||
)
|
||||
expect(screen.getByRole('textbox', { name: '不可用多行输入' })).not.toHaveClass(
|
||||
'shadow-surface'
|
||||
)
|
||||
expect(screen.getByRole('checkbox', { name: '不可用多选' })).toHaveClass(
|
||||
'border-muted-foreground/60'
|
||||
)
|
||||
@@ -148,6 +160,8 @@ describe('TraceMemo UI control states', () => {
|
||||
expect(screen.getByRole('radio', { name: '舒适密度' })).toBeChecked()
|
||||
|
||||
const toggle = screen.getByRole('switch', { name: '显示头像' })
|
||||
expect(toggle).toHaveClass('h-5', 'w-9', 'border-0', 'p-0.5')
|
||||
expect(toggle.firstElementChild).toHaveClass('h-4', 'w-4')
|
||||
await user.click(toggle)
|
||||
expect(toggle).toBeChecked()
|
||||
})
|
||||
|
||||
@@ -79,5 +79,10 @@ describe('TraceMemo UI infrastructure', () => {
|
||||
await user.click(screen.getByRole('button', { name: '显示通知' }))
|
||||
expect(screen.getByText('已保存')).toBeVisible()
|
||||
expect(screen.getByText('设置已更新')).toBeVisible()
|
||||
|
||||
const close = screen.getByRole('button', { name: '关闭通知' })
|
||||
expect(close).toHaveClass('h-7', 'w-7', 'border-0', 'bg-transparent')
|
||||
await user.click(close)
|
||||
expect(screen.queryByText('已保存')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -93,7 +93,13 @@ describe('voice recognition settings', () => {
|
||||
...window.api,
|
||||
getContacts: vi.fn().mockResolvedValue([
|
||||
{ md5: 'contact-a', m_nsNickName: '联系人 A', m_nsUsrName: 'contact-a', type: 'user' },
|
||||
{ md5: 'group-b', m_nsNickName: '群聊 B', m_nsUsrName: 'group-b@chatroom', type: 'group' }
|
||||
{
|
||||
md5: 'group-b',
|
||||
m_nsNickName: '群聊 B',
|
||||
m_nsUsrName: 'group-b@chatroom',
|
||||
type: 'group',
|
||||
avatar: 'data:image/png;base64,voice-fixture-avatar'
|
||||
}
|
||||
]),
|
||||
getSelf: vi.fn().mockResolvedValue({
|
||||
ready: true,
|
||||
@@ -142,6 +148,9 @@ describe('voice recognition settings', () => {
|
||||
|
||||
expect(await screen.findByText('群聊 B')).toBeInTheDocument()
|
||||
expect(await screen.findByText('7 条语音')).toBeInTheDocument()
|
||||
expect(
|
||||
document.querySelector('img[src="data:image/png;base64,voice-fixture-avatar"]')
|
||||
).toBeInTheDocument()
|
||||
expect(window.api.getVoiceBatchConversationSummaries).toHaveBeenCalledWith({
|
||||
conversationIds: ['group-b'],
|
||||
range: 'recent_30_days'
|
||||
|
||||
@@ -307,6 +307,7 @@ test('SETTINGS-02 basic settings controls fit a narrow viewport and keep their s
|
||||
.click()
|
||||
|
||||
await expect(fixture.page.getByRole('heading', { name: '账号与数据库' })).toBeVisible()
|
||||
await expect(fixture.page.getByRole('button', { name: '存储与导出' })).toHaveCount(0)
|
||||
const autoLoginSwitch = fixture.page.getByRole('switch', { name: '启动时自动连接数据库' })
|
||||
await expect(autoLoginSwitch).toBeVisible()
|
||||
await autoLoginSwitch.click()
|
||||
@@ -438,6 +439,12 @@ test('SETTINGS-05 AI model editor keeps provider and capability semantics', asyn
|
||||
await expect(fixture.page.getByRole('heading', { name: 'AI 模型' })).toBeVisible()
|
||||
await expect(fixture.page.getByRole('button', { name: '当前默认' })).toBeDisabled()
|
||||
|
||||
await fixture.page.getByRole('button', { name: '编辑' }).click()
|
||||
await expect(fixture.page.getByRole('heading', { name: '编辑供应商' })).toBeVisible()
|
||||
await expect(fixture.page.getByLabel('供应商名称')).toBeFocused()
|
||||
await expect(fixture.page.locator('#ai-provider-editor')).toBeInViewport()
|
||||
await fixture.page.getByRole('button', { name: '关闭' }).click()
|
||||
|
||||
await fixture.page.getByRole('button', { name: '添加供应商' }).click()
|
||||
await expect(fixture.page.getByRole('heading', { name: '新增供应商' })).toBeVisible()
|
||||
await expect(fixture.page.getByLabel('供应商名称')).toBeFocused()
|
||||
@@ -493,6 +500,7 @@ test('SETTINGS-07 voice recognition controls keep selection semantics at a narro
|
||||
|
||||
const categoryTabs = fixture.page.getByRole('tablist', { name: '会话类别' })
|
||||
await categoryTabs.scrollIntoViewIfNeeded()
|
||||
await expect(fixture.page.locator('.voice-conversation-avatar img')).toHaveCount(1)
|
||||
await expect(categoryTabs.getByRole('tab', { name: /群聊/ })).toHaveAttribute(
|
||||
'data-state',
|
||||
'active'
|
||||
@@ -826,6 +834,15 @@ test('ASK-01 uses the local fixed AI service and keeps evidence in the UI', asyn
|
||||
await expect(fixture.page.getByText(/固定假回答:测试数据中的核心流程正常/)).toBeVisible({
|
||||
timeout: 15_000
|
||||
})
|
||||
|
||||
await fixture.page.getByRole('button', { name: /历史提问/ }).click()
|
||||
await fixture.page.getByRole('button', { name: '测试群讨论了什么?', exact: true }).click()
|
||||
const toastClose = fixture.page.getByRole('button', { name: '关闭通知' })
|
||||
await expect(toastClose).toBeVisible()
|
||||
expect(await toastClose.boundingBox()).toMatchObject({ width: 28, height: 28 })
|
||||
await toastClose.click()
|
||||
await expect(toastClose).toHaveCount(0)
|
||||
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
).toBe(true)
|
||||
|
||||
@@ -55,6 +55,7 @@ export async function launchTestApp(
|
||||
})
|
||||
const page = await app.firstWindow()
|
||||
await page.waitForLoadState('domcontentloaded')
|
||||
if (options.now) await page.clock.setFixedTime(options.now)
|
||||
const setWindowContentSize = async (size: { width: number; height: number }): Promise<void> => {
|
||||
await app.evaluate(({ BrowserWindow, screen }, nextSize) => {
|
||||
const [window] = BrowserWindow.getAllWindows()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SETTINGS_NAVIGATION } from '../../src/renderer/src/features/settings/model/settingsNavigation'
|
||||
import {
|
||||
SETTINGS_CATEGORY_LABELS,
|
||||
SETTINGS_NAVIGATION
|
||||
} from '../../src/renderer/src/features/settings/model/settingsNavigation'
|
||||
|
||||
describe('settings navigation', () => {
|
||||
it('places text-to-speech under intelligent capabilities', () => {
|
||||
@@ -10,4 +13,11 @@ describe('settings navigation', () => {
|
||||
'ai-model'
|
||||
])
|
||||
})
|
||||
|
||||
it('temporarily hides storage and export from visible navigation', () => {
|
||||
expect(SETTINGS_NAVIGATION.flatMap((group) => group.items)).not.toEqual(
|
||||
expect.arrayContaining([{ id: 'storage-export', label: '存储与导出' }])
|
||||
)
|
||||
expect(SETTINGS_CATEGORY_LABELS['storage-export']).toBe('存储与导出')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user