diff --git a/web/UI/AI模式.png b/web/UI/AI模式.png new file mode 100644 index 0000000..692ec54 Binary files /dev/null and b/web/UI/AI模式.png differ diff --git a/web/src/assets/styles/app.css b/web/src/assets/styles/app.css index 8739a72..50f7a36 100644 --- a/web/src/assets/styles/app.css +++ b/web/src/assets/styles/app.css @@ -15,7 +15,7 @@ } .app { - --sidebar-expanded-width: 184px; + --sidebar-expanded-width: 304px; --sidebar-collapsed-width: 64px; --sidebar-motion: 220ms cubic-bezier(0.4, 0, 0.2, 1); @@ -43,8 +43,8 @@ } .app.sidebar-collapsed .app-sidebar { - flex-basis: var(--sidebar-collapsed-width); width: var(--sidebar-collapsed-width); + flex-basis: var(--sidebar-collapsed-width); overflow: visible; z-index: 200; } @@ -54,6 +54,19 @@ z-index: 1; } +.sidebar-mode-fade-enter-active, +.sidebar-mode-fade-leave-active { + transition: + opacity 180ms var(--ease), + transform 180ms var(--ease); +} + +.sidebar-mode-fade-enter-from, +.sidebar-mode-fade-leave-to { + opacity: 0; + transform: translateX(-8px); +} + .app > .main { flex: 1 1 auto; min-width: 0; @@ -133,7 +146,7 @@ color: var(--theme-primary-active); font-size: 12px; font-weight: 800; - letter-spacing: 0.12em; + letter-spacing: 0; } .boot-badge-error { @@ -217,6 +230,10 @@ background-size: 100% 100%, 100% 100%, 32px 32px, 32px 32px; background-attachment: local; } +.workarea.workbench-workarea.workbench-workarea-ai-mode { + padding: 0; + background: transparent; +} .workarea.settings-workarea { padding: 0; background: #fff; @@ -312,6 +329,7 @@ } .workarea.workbench-workarea { overflow: auto; padding: 16px; } + .workarea.workbench-workarea.workbench-workarea-ai-mode { padding: 0; } .mobile-overlay { position: fixed; diff --git a/web/src/assets/styles/components/ai-sidebar-rail.css b/web/src/assets/styles/components/ai-sidebar-rail.css new file mode 100644 index 0000000..c577030 --- /dev/null +++ b/web/src/assets/styles/components/ai-sidebar-rail.css @@ -0,0 +1,676 @@ +.ai-rail { + --ai-rail-bg: #f7f9fc; + --ai-rail-panel: rgba(255, 255, 255, 0.76); + --ai-rail-line: rgba(148, 163, 184, 0.14); + --ai-rail-text: #162033; + --ai-rail-muted: #738097; + --ai-rail-accent: #2d72d9; + --ai-rail-amber: #b76b16; + --ai-rail-green: #2f8d7b; + --ai-rail-ink-soft: #41506a; + --ai-rail-accent-soft: rgba(45, 114, 217, 0.08); + + position: sticky; + top: 0; + width: 100%; + height: var(--desktop-stage-height, 100dvh); + min-height: var(--desktop-stage-height, 100dvh); + display: grid; + grid-template-rows: auto auto auto auto auto minmax(0, 1fr) auto; + overflow: hidden; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(247, 249, 252, 0.96) 62%, rgba(244, 247, 251, 0.98)), + var(--ai-rail-bg); + border-right: 1px solid rgba(203, 213, 225, 0.54); + box-shadow: + inset -1px 0 0 rgba(255, 255, 255, 0.64), + 1px 0 0 rgba(15, 23, 42, 0.02); + color: var(--ai-rail-text); + contain: layout paint; +} + +.ai-rail::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.56), transparent 16%), + repeating-linear-gradient( + 180deg, + rgba(255, 255, 255, 0.12) 0, + rgba(255, 255, 255, 0.12) 1px, + transparent 1px, + transparent 20px + ); + opacity: 0.22; +} + +.ai-rail > * { + position: relative; + z-index: 1; +} + +.ai-rail-section { + min-width: 0; +} + +.ai-rail-brand { + min-width: 0; + min-height: 74px; + display: grid; + grid-template-columns: 42px minmax(0, 1fr); + align-items: center; + gap: 12px; + padding: 16px 18px 10px; +} + +.ai-brand-logo { + width: 42px; + height: 42px; + display: grid; + place-items: center; + border: 1px solid rgba(45, 114, 217, 0.12); + border-radius: 13px; + background: + linear-gradient(145deg, rgba(255, 255, 255, 0.84), rgba(239, 246, 255, 0.7)), + rgba(255, 255, 255, 0.72); + color: var(--ai-rail-accent); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.86), + 0 8px 18px rgba(45, 114, 217, 0.055); +} + +.ai-brand-logo img { + width: 28px; + height: 28px; + object-fit: contain; +} + +.ai-brand-logo svg { + width: 26px; + height: 26px; + fill: currentColor; +} + +.ai-brand-copy { + min-width: 0; + display: grid; + gap: 3px; +} + +.ai-brand-copy strong { + overflow: hidden; + color: #162033; + font-size: 14px; + font-weight: 820; + line-height: 1.22; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-brand-copy small { + overflow: hidden; + color: var(--ai-rail-muted); + font-size: 12px; + font-weight: 560; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-rail-quick { + display: grid; + gap: 6px; + padding: 8px 18px 12px; +} + +.ai-quick-btn, +.ai-nav-btn, +.ai-recent-item, +.ai-user-action { + width: 100%; + min-width: 0; + border: 1px solid transparent; + border-radius: 10px; + background: transparent; + color: inherit; + cursor: pointer; + text-align: left; + transition: + background 180ms var(--ease), + border-color 180ms var(--ease), + box-shadow 180ms var(--ease), + color 180ms var(--ease), + transform 180ms var(--ease); +} + +.ai-quick-btn { + min-height: 48px; + display: grid; + grid-template-columns: 28px minmax(0, 1fr); + align-items: center; + gap: 10px; + padding: 0 4px; + color: #111827; + font-size: 14px; + font-weight: 780; + background: transparent; + border-color: transparent; + box-shadow: none; +} + +.ai-quick-btn i { + width: 28px; + display: inline-flex; + justify-content: center; + color: #536277; + font-size: 18px; + line-height: 1; +} + +.ai-quick-btn.primary { + background: transparent; + border-color: transparent; + box-shadow: none; +} + +.ai-quick-btn.active { + color: #173d78; + background: rgba(45, 114, 217, 0.055); + border-color: rgba(45, 114, 217, 0.12); +} + +.ai-quick-btn.primary i { + color: var(--ai-rail-amber); +} + +.ai-nav-btn:hover, +.ai-recent-item:hover, +.ai-user-action:hover { + background: rgba(255, 255, 255, 0.78); + border-color: rgba(148, 163, 184, 0.28); + box-shadow: 0 8px 18px rgba(31, 48, 68, 0.045); + transform: translateY(-1px); +} + +.ai-quick-btn:hover { + color: #0f172a; + background: rgba(15, 23, 42, 0.035); + border-color: transparent; + box-shadow: none; + transform: translateX(2px); +} + +.ai-quick-btn:hover i { + color: var(--ai-rail-accent); +} + +.ai-quick-btn.primary:hover i { + color: var(--ai-rail-amber); +} + +.ai-conversation-search { + min-width: 0; + min-height: 48px; + height: 48px; + display: grid; + grid-template-columns: 28px minmax(0, 1fr) 28px; + align-items: center; + gap: 4px; + padding: 0 6px 0 4px; + border: 1px solid rgba(45, 114, 217, 0.14); + border-radius: 12px; + background: rgba(255, 255, 255, 0.7); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.8), + 0 8px 18px rgba(45, 114, 217, 0.035); +} + +.ai-conversation-search > i { + color: #64748b; + font-size: 17px; + line-height: 1; +} + +.ai-conversation-search input { + min-width: 0; + width: 100%; + border: 0; + outline: 0; + background: transparent; + color: #162033; + font-size: 13px; + font-weight: 650; + letter-spacing: 0; +} + +.ai-conversation-search input::placeholder { + color: rgba(115, 128, 151, 0.78); +} + +.ai-conversation-search button { + width: 28px; + height: 28px; + display: grid; + place-items: center; + padding: 0; + border: 0; + border-radius: 8px; + background: transparent; + color: #64748b; + cursor: pointer; +} + +.ai-conversation-search button:hover { + background: rgba(15, 23, 42, 0.055); + color: #173d78; +} + +.ai-rail-divider { + height: 1px; + margin: 0 18px; + background: var(--ai-rail-line); +} + +.ai-section-heading { + margin: 0; + padding: 0 10px 8px; + color: #7d8796; + font-size: 12px; + font-weight: 760; + letter-spacing: 0; +} + +.ai-rail-nav { + display: grid; + padding: 18px 18px 20px; +} + +.ai-nav-list { + position: relative; + display: grid; + gap: 6px; + padding: 2px 0; +} + +.ai-nav-btn { + position: relative; + min-height: 48px; + display: grid; + grid-template-columns: 32px minmax(0, 1fr); + align-items: center; + gap: 12px; + padding: 7px 10px; + color: var(--ai-rail-ink-soft); + border-radius: 12px; + background: transparent; + box-shadow: none; +} + +.ai-nav-btn::before { + content: ""; + position: absolute; + left: 0; + width: 3px; + height: 22px; + border-radius: 999px; + background: transparent; + transition: + background 180ms var(--ease), + opacity 180ms var(--ease); + opacity: 0; +} + +.ai-nav-btn.active { + border-color: rgba(45, 114, 217, 0.13); + background: + linear-gradient(90deg, rgba(45, 114, 217, 0.095), rgba(255, 255, 255, 0.74)), + var(--ai-rail-panel); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.78), + 0 8px 18px rgba(45, 114, 217, 0.045); + color: #173d78; +} + +.ai-nav-btn.active::before { + background: linear-gradient(180deg, var(--ai-rail-accent), var(--ai-rail-green)); + opacity: 1; +} + +.ai-nav-btn:not(.active):hover::before { + background: rgba(45, 114, 217, 0.36); + opacity: 1; +} + +.ai-nav-icon { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border-radius: 10px; + background: transparent; + color: #64748b; + transition: + background 180ms var(--ease), + color 180ms var(--ease), + box-shadow 180ms var(--ease); +} + +.ai-nav-btn.active .ai-nav-icon { + background: + linear-gradient(145deg, rgba(45, 114, 217, 0.12), rgba(255, 255, 255, 0.72)), + rgba(255, 255, 255, 0.52); + color: var(--ai-rail-accent); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82); +} + +.ai-nav-icon i { + font-size: 18px; + line-height: 1; +} + +.ai-nav-copy { + min-width: 0; +} + +.ai-nav-copy strong, +.ai-recent-title { + min-width: 0; + overflow: hidden; + color: currentColor; + font-size: 14px; + font-weight: 780; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-nav-btn.active .ai-nav-copy strong { + font-weight: 820; +} + +.ai-recent-desc { + min-width: 0; + overflow: hidden; + color: var(--ai-rail-muted); + font-size: 12px; + font-weight: 500; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-rail-recents { + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 8px; + overflow: hidden; + padding: 18px 12px 12px; +} + +.ai-recents-list { + min-height: 0; + display: grid; + align-content: start; + gap: 5px; + overflow-y: auto; + padding: 1px 4px 1px 0; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.ai-recents-list::-webkit-scrollbar { + width: 0; + height: 0; + display: none; +} + +.ai-recents-empty { + margin: 10px 8px 0 12px; + padding: 14px 12px; + border: 1px dashed rgba(148, 163, 184, 0.22); + border-radius: 12px; + color: rgba(115, 128, 151, 0.84); + font-size: 12px; + font-weight: 650; + line-height: 1.5; +} + +.ai-recent-item { + min-height: 56px; + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + gap: 8px; + padding: 8px 10px 8px 18px; +} + +.ai-recent-item:focus-visible { + outline: 2px solid rgba(45, 114, 217, 0.32); + outline-offset: 2px; +} + +.ai-recent-item::before { + content: ""; + position: absolute; + left: 6px; + top: 16px; + width: 5px; + height: 5px; + border-radius: 50%; + background: rgba(115, 128, 151, 0.38); +} + +.ai-recent-main { + min-width: 0; + display: grid; + gap: 4px; +} + +.ai-recent-item:hover .ai-recent-title, +.ai-recent-item.active .ai-recent-title { + color: #173d78; +} + +.ai-recent-item:hover::before, +.ai-recent-item.active::before { + background: var(--ai-rail-accent); +} + +.ai-recent-item.active { + background: rgba(255, 255, 255, 0.72); + border-color: rgba(183, 107, 22, 0.1); + box-shadow: + 0 10px 22px rgba(31, 48, 68, 0.055), + inset 0 1px 0 rgba(255, 255, 255, 0.9); +} + +.ai-recent-time { + color: rgba(107, 114, 128, 0.82); + font-size: 11px; + font-weight: 680; + line-height: 1.35; +} + +.ai-recent-title-input { + width: 100%; + min-width: 0; + height: 24px; + padding: 0 6px; + border: 1px solid rgba(45, 114, 217, 0.22); + border-radius: 7px; + outline: 0; + background: rgba(255, 255, 255, 0.86); + color: #173d78; + font-size: 14px; + font-weight: 780; + line-height: 1.2; + letter-spacing: 0; + box-shadow: 0 0 0 3px rgba(45, 114, 217, 0.06); +} + +.ai-rail-user { + box-sizing: border-box; + min-width: 0; + height: 72px; + min-height: 72px; + display: grid; + grid-template-columns: 42px minmax(0, 1fr) 44px; + align-items: center; + gap: 12px; + margin: 0; + padding: 12px 14px 12px 18px; + border-top: 1px solid rgba(203, 213, 225, 0.55); + border-radius: 0; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.76), rgba(247, 250, 252, 0.9)), + rgba(255, 255, 255, 0.72); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84); +} + +.ai-user-avatar { + width: 42px; + height: 42px; + display: grid; + place-items: center; + border: 2px solid rgba(255, 255, 255, 0.92); + border-radius: 50%; + background: + radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.22), transparent 32%), + linear-gradient(135deg, #1f4f96, #2f8d7b); + color: #fff; + font-size: 15px; + font-weight: 820; + box-shadow: + 0 8px 16px rgba(45, 114, 217, 0.13), + inset 0 -1px 0 rgba(15, 23, 42, 0.08); +} + +.ai-user-copy { + min-width: 0; + display: grid; + gap: 2px; +} + +.ai-user-copy strong { + overflow: hidden; + color: #182237; + font-size: 13px; + font-weight: 760; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-user-copy span { + overflow: hidden; + color: var(--ai-rail-muted); + font-size: 12px; + font-weight: 520; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-user-actions { + display: grid; + grid-template-columns: 44px; + justify-content: end; +} + +.ai-user-action { + width: 44px; + height: 44px; + display: grid; + place-items: center; + padding: 0; + color: #708096; + border-radius: 8px; + background: transparent; + box-shadow: none; +} + +.ai-user-action i { + font-size: 19px; +} + +.ai-rail.rail-collapsed { + grid-template-rows: auto auto auto auto minmax(0, 1fr) auto; +} + +.ai-rail.rail-collapsed .ai-rail-brand { + grid-template-columns: 1fr; + justify-items: center; + min-height: 70px; + padding: 14px 10px 8px; +} + +.ai-rail.rail-collapsed .ai-rail-quick { + padding: 4px 10px 16px; +} + +.ai-rail.rail-collapsed .ai-nav-list { + grid-template-columns: 1fr; + gap: 8px; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; +} + +.ai-rail.rail-collapsed .ai-nav-list::before, +.ai-rail.rail-collapsed .ai-nav-btn::before { + display: none; +} + +.ai-rail.rail-collapsed .ai-quick-btn, +.ai-rail.rail-collapsed .ai-nav-btn { + min-height: 44px; + grid-template-rows: auto; + justify-content: center; + grid-template-columns: 1fr; + align-content: center; + padding: 8px; +} + +.ai-rail.rail-collapsed .ai-nav-btn.active { + grid-column: auto; + min-height: 44px; + grid-template-columns: 1fr; +} + +.ai-rail.rail-collapsed .ai-quick-btn span, +.ai-rail.rail-collapsed .ai-conversation-search, +.ai-rail.rail-collapsed .ai-brand-copy, +.ai-rail.rail-collapsed .ai-section-heading, +.ai-rail.rail-collapsed .ai-nav-copy, +.ai-rail.rail-collapsed .ai-rail-recents, +.ai-rail.rail-collapsed .ai-user-copy, +.ai-rail.rail-collapsed .ai-user-actions { + display: none; +} + +.ai-rail.rail-collapsed .ai-quick-btn i, +.ai-rail.rail-collapsed .ai-brand-logo, +.ai-rail.rail-collapsed .ai-nav-icon, +.ai-rail.rail-collapsed .ai-user-avatar { + margin: 0 auto; +} + +.ai-rail.rail-collapsed .ai-rail-user { + grid-template-columns: 1fr; + padding: 12px 10px 14px; +} + +@media (max-width: 760px) { + .ai-rail { + max-width: min(320px, 82vw); + } + + .ai-rail-quick { + padding-top: 18px; + } +} diff --git a/web/src/assets/styles/components/personal-workbench-ai-mode.css b/web/src/assets/styles/components/personal-workbench-ai-mode.css new file mode 100644 index 0000000..a313306 --- /dev/null +++ b/web/src/assets/styles/components/personal-workbench-ai-mode.css @@ -0,0 +1,1446 @@ +.workbench-ai-mode { + --ai-ink: #0f172a; + --ai-text: #334155; + --ai-muted: #738199; + --ai-line: #dbe7f6; + --ai-blue: #2f7cff; + --ai-blue-deep: #1f3b66; + --ai-purple: #7c5cff; + --ai-cyan: #15b8c8; + --ai-amber: #f59e0b; + --ai-theme-rgb: var(--theme-primary-rgb, 58, 124, 165); + + position: relative; + min-height: 100%; + height: 100%; + width: 100%; + overflow: hidden; + display: grid; + place-items: safe center; + padding: clamp(24px, 4vh, 56px) 36px; + color: var(--ai-ink); + background: + radial-gradient(circle at 17% 4%, rgba(var(--ai-theme-rgb), 0.13), transparent 42%), + radial-gradient(circle at 88% 96%, rgba(245, 158, 11, 0.18), transparent 34%), + linear-gradient(115deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.84) 55%, rgba(255, 247, 237, 0.86)), + var(--bg); + isolation: isolate; +} + +.workbench-ai-mode::after { + content: ""; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background: + linear-gradient(90deg, rgba(var(--ai-theme-rgb), 0.045) 1px, transparent 1px), + linear-gradient(180deg, rgba(var(--ai-theme-rgb), 0.04) 1px, transparent 1px), + radial-gradient(circle at 74% 24%, rgba(var(--ai-theme-rgb), 0.1), transparent 30%), + radial-gradient(circle at 94% 72%, rgba(251, 191, 36, 0.14), transparent 28%); + background-size: 56px 56px, 56px 56px, auto, auto; + opacity: 0.74; +} + +.workbench-ai-mode.has-conversation { + place-items: stretch; + padding: 0; + background: + radial-gradient(circle at 88% 96%, rgba(245, 158, 11, 0.16), transparent 34%), + radial-gradient(circle at 12% 0%, rgba(var(--ai-theme-rgb), 0.14), transparent 38%), + linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(248, 250, 252, 0.94)); +} + +.workbench-ai-mode.has-conversation::after { + opacity: 0.58; + filter: saturate(0.92); +} + +.workbench-ai-mode, +.workbench-ai-mode * { + box-sizing: border-box; +} + +.workbench-ai-mode :where(button, textarea) { + font: inherit; +} + +.workbench-ai-shell { + position: relative; + z-index: 1; + width: min(1180px, 100%); + display: grid; + justify-items: center; + align-content: center; + gap: 26px; +} + +.workbench-ai-orb { + width: clamp(118px, 8vw, 132px); + height: clamp(118px, 8vw, 132px); + display: grid; + place-items: center; + border-radius: 50%; + border: 1px solid rgba(47, 124, 255, 0.18); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(240, 247, 255, 0.84)), + linear-gradient(135deg, rgba(21, 184, 200, 0.14), rgba(124, 92, 255, 0.1)); + color: var(--ai-blue); + box-shadow: + 0 24px 50px rgba(47, 124, 255, 0.18), + 0 0 0 8px rgba(47, 124, 255, 0.045), + inset 0 1px 0 rgba(255, 255, 255, 0.96); + overflow: hidden; + animation: workbenchAiControlIn 520ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; + animation-delay: 80ms; +} + +.workbench-ai-orb__image { + width: 100%; + height: 100%; + display: block; + object-fit: contain; + object-position: center center; +} + +.workbench-ai-copy { + display: grid; + gap: 8px; + text-align: center; + animation: workbenchAiControlIn 520ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; + animation-delay: 160ms; +} + +.workbench-ai-copy h2 { + margin: 0; + color: var(--ai-ink); + font-size: 28px; + line-height: 1.18; + font-weight: 900; + letter-spacing: 0; +} + +.workbench-ai-copy p { + margin: 0; + color: var(--ai-muted); + font-size: 16px; + line-height: 1.7; + font-weight: 650; +} + +.workbench-ai-file-input { + display: none; +} + +.workbench-ai-composer { + width: min(980px, 100%); + min-height: 154px; + display: grid; + grid-template-rows: minmax(80px, 1fr) auto; + gap: 14px; + margin-top: 14px; + padding: 22px 24px; + border: 1px solid rgba(211, 224, 242, 0.74); + border-radius: 20px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 251, 247, 0.94)); + box-shadow: + 0 18px 60px rgba(15, 23, 42, 0.08), + 0 4px 14px rgba(15, 23, 42, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.96); + animation: workbenchAiControlIn 540ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; + animation-delay: 250ms; +} + +.workbench-ai-composer-field { + min-height: 0; + display: flex; + flex-direction: column; + gap: 10px; +} + +.workbench-ai-composer textarea { + width: 100%; + min-height: 80px; + resize: none; + border: 0; + outline: 0; + background: transparent; + color: #1f2937; + font-size: 17px; + line-height: 1.65; + font-weight: 540; + animation: workbenchAiControlIn 460ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; + animation-delay: 310ms; +} + +.workbench-ai-composer textarea::placeholder { + color: #a7b2c5; +} + +.workbench-ai-composer-toolbar { + display: flex; + justify-content: space-between; + align-items: center; +} + +.workbench-ai-tool-buttons { + display: inline-flex; + align-items: center; + gap: 16px; +} + +.workbench-ai-composer-right { + display: inline-flex; + align-items: center; + gap: 16px; +} + +.workbench-ai-model-selector { + display: inline-flex; + align-items: center; + gap: 6px; + color: #475569; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: color 180ms ease; +} + +.workbench-ai-model-selector:hover { + color: #0f172a; +} + +.workbench-ai-icon-btn, +.workbench-ai-send-btn, +.workbench-ai-action, +.workbench-ai-file-strip button { + border: 0; + cursor: pointer; + transition: + transform 180ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)), + box-shadow 180ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)), + border-color 180ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)), + background 180ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)), + color 180ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)); +} + +.workbench-ai-icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + color: #64748b; + font-size: 20px; + width: 36px; + height: 36px; + padding: 0; + border-radius: 10px; + box-shadow: none; + animation: workbenchAiControlIn 440ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; + animation-delay: 360ms; +} + +.workbench-ai-icon-btn + .workbench-ai-icon-btn { + animation-delay: 400ms; +} + +.workbench-ai-icon-btn:hover { + color: var(--ai-blue-deep); + background: rgba(47, 124, 255, 0.08); + transform: none; + box-shadow: none; +} + +.workbench-ai-icon-btn.active { + color: #175cd3; + background: rgba(47, 124, 255, 0.11); +} + +.workbench-ai-send-btn { + width: 36px; + height: 36px; + display: inline-grid; + place-items: center; + border-radius: 50%; + background: #e5e7eb; + color: #94a3b8; + font-size: 18px; + box-shadow: none; + animation: workbenchAiControlIn 440ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; + animation-delay: 450ms; +} + +.workbench-ai-send-btn:disabled { + opacity: 1; + cursor: not-allowed; + box-shadow: none; +} + +.workbench-ai-send-btn:not(:disabled) { + background: + linear-gradient(135deg, #1d4ed8 0%, #2563eb 54%, #0891b2 100%); + color: #ffffff; + box-shadow: + 0 12px 24px rgba(37, 99, 235, 0.24), + inset 0 1px 0 rgba(255, 255, 255, 0.38); +} + +.workbench-ai-send-btn:not(:disabled):hover { + transform: translateY(-1px); + background: + linear-gradient(135deg, #1e40af 0%, #1d4ed8 54%, #0e7490 100%); +} + +.workbench-ai-action:hover, +.workbench-ai-file-strip button:hover { + transform: translateY(-1px); + border-color: rgba(47, 124, 255, 0.1); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05); +} + +.workbench-ai-date-anchor { + position: relative; + display: inline-flex; +} + +.workbench-ai-date-chip { + width: fit-content; + max-width: 100%; + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 30px; + padding: 0 8px 0 10px; + border: 1px solid rgba(47, 124, 255, 0.18); + border-radius: 999px; + background: rgba(239, 246, 255, 0.9); + color: #1d4ed8; + font-size: 13px; + font-weight: 780; + line-height: 1; +} + +.workbench-ai-date-chip button { + width: 22px; + height: 22px; + display: inline-grid; + place-items: center; + border: 0; + border-radius: 50%; + background: transparent; + color: inherit; + cursor: pointer; +} + +.workbench-ai-date-chip button:hover { + background: rgba(47, 124, 255, 0.12); +} + +.workbench-ai-date-popover { + position: absolute; + left: 0; + bottom: calc(100% + 12px); + z-index: 20; + width: 276px; + padding: 14px; + border: 1px solid rgba(203, 213, 225, 0.78); + border-radius: 16px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.98)); + box-shadow: + 0 22px 55px rgba(15, 23, 42, 0.16), + inset 0 1px 0 rgba(255, 255, 255, 0.96); + animation: workbenchAiPopoverIn 180ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) both; +} + +.workbench-ai-date-tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px; + padding: 4px; + margin-bottom: 12px; + border-radius: 12px; + background: rgba(226, 232, 240, 0.62); +} + +.workbench-ai-date-tabs button, +.workbench-ai-date-actions button { + border: 0; + cursor: pointer; + font: inherit; +} + +.workbench-ai-date-tabs button { + min-height: 34px; + border-radius: 9px; + background: transparent; + color: #64748b; + font-size: 13px; + font-weight: 820; +} + +.workbench-ai-date-tabs button.active { + background: #ffffff; + color: #0f172a; + box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08); +} + +.workbench-ai-date-range { + display: grid; + gap: 10px; +} + +.workbench-ai-date-field { + display: grid; + gap: 6px; +} + +.workbench-ai-date-field span { + color: #64748b; + font-size: 12px; + font-weight: 760; +} + +.workbench-ai-date-field input { + min-height: 38px; + padding: 0 10px; + border: 1px solid rgba(203, 213, 225, 0.84); + border-radius: 10px; + background: #ffffff; + color: #1f2937; + font: inherit; + font-size: 13px; +} + +.workbench-ai-date-field input:focus { + outline: 0; + border-color: rgba(47, 124, 255, 0.55); + box-shadow: 0 0 0 3px rgba(47, 124, 255, 0.12); +} + +.workbench-ai-date-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} + +.workbench-ai-date-actions button { + min-height: 34px; + padding: 0 12px; + border-radius: 10px; + font-size: 13px; + font-weight: 820; +} + +.workbench-ai-date-actions .ghost { + background: transparent; + color: #64748b; +} + +.workbench-ai-date-actions .primary { + background: #1d4ed8; + color: #ffffff; + box-shadow: 0 10px 18px rgba(29, 78, 216, 0.2); +} + +.workbench-ai-date-actions .primary:disabled { + background: #e2e8f0; + color: #94a3b8; + cursor: not-allowed; + box-shadow: none; +} + +.workbench-ai-file-strip { + width: min(980px, 100%); + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + color: var(--ai-muted); + font-size: 13px; + font-weight: 700; + animation: workbenchAiControlIn 480ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; + animation-delay: 340ms; +} + +.workbench-ai-file-strip button { + min-height: 30px; + padding: 0 12px; + border: 1px solid rgba(47, 124, 255, 0.18); + border-radius: 8px; + background: rgba(255, 255, 255, 0.82); + color: var(--ai-blue); + font-size: 13px; + font-weight: 800; +} + +.workbench-ai-quick-start-section { + width: min(980px, 100%); + margin-top: 16px; + display: flex; + flex-direction: column; +} + +.workbench-ai-quick-start-title { + font-size: 12px; + font-weight: 700; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0; + margin: 0 0 16px 12px; + text-align: left; +} + +.workbench-ai-action-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + width: 100%; + margin-top: 0; +} + +.workbench-ai-action { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + gap: 16px; + padding: 20px; + border: 1px solid rgba(0, 0, 0, 0.03); + border-radius: 8px; + background: rgba(255, 255, 255, 0.6); + color: inherit; + white-space: normal; + box-shadow: + 0 4px 12px rgba(0, 0, 0, 0.02), + inset 0 1px 0 rgba(255, 255, 255, 0.92); + animation: workbenchAiControlIn 480ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; + animation-delay: 370ms; +} + +.action-icon-wrapper { + width: 36px; + height: 36px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + display: grid; + place-items: center; + flex-shrink: 0; +} + +.action-icon-wrapper i { + color: var(--ai-blue); + font-size: 18px; +} + +.workbench-ai-action:nth-child(2) { + animation-delay: 420ms; +} + +.workbench-ai-action:nth-child(3) { + animation-delay: 470ms; +} + +.workbench-ai-action:nth-child(4) { + animation-delay: 520ms; +} + +.workbench-ai-action:nth-child(2) .action-icon-wrapper i { + color: var(--ai-amber); +} + +.workbench-ai-action:nth-child(3) .action-icon-wrapper i { + color: var(--ai-amber); +} + +.workbench-ai-action:nth-child(4) .action-icon-wrapper i { + color: var(--ai-amber); +} + +.action-text { + display: flex; + flex-direction: column; + gap: 6px; + text-align: left; +} + +.action-text strong { + font-size: 15px; + font-weight: 700; + color: #1e293b; + line-height: 1.3; +} + +.action-text p { + font-size: 13px; + color: #64748b; + font-weight: 500; + line-height: 1.5; + margin: 0; +} + +.workbench-ai-panel-swap-enter-active, +.workbench-ai-panel-swap-leave-active { + transition: + opacity 220ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)), + transform 220ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)); +} + +.workbench-ai-panel-swap-enter-from, +.workbench-ai-panel-swap-leave-to { + opacity: 0; + transform: translateY(10px) scale(0.992); +} + +.workbench-ai-conversation { + position: relative; + z-index: 1; + min-height: 0; + height: 100%; + display: grid; + grid-template-rows: minmax(0, 1fr) auto; + padding: clamp(24px, 4vh, 42px) clamp(28px, 8vw, 132px) 26px; + overflow: hidden; +} + +.workbench-ai-conversation-actions { + position: absolute; + top: clamp(18px, 3vh, 30px); + right: clamp(22px, 6vw, 96px); + z-index: 8; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 5px; + border: 1px solid rgba(226, 232, 240, 0.72); + border-radius: 999px; + background: rgba(255, 255, 255, 0.78); + box-shadow: + 0 12px 34px rgba(15, 23, 42, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.88); + backdrop-filter: blur(16px); +} + +.workbench-ai-conversation-actions button { + width: 34px; + height: 34px; + display: inline-grid; + place-items: center; + border: 0; + border-radius: 50%; + background: transparent; + color: #64748b; + cursor: pointer; + transition: + background 180ms ease, + color 180ms ease, + transform 180ms ease; +} + +.workbench-ai-conversation-actions button:hover { + color: #0f172a; + background: rgba(15, 23, 42, 0.055); + transform: translateY(-1px); +} + +.workbench-ai-conversation-actions button.danger:hover { + color: #b42318; + background: rgba(220, 38, 38, 0.08); +} + +.workbench-ai-conversation-actions button:disabled { + color: #cbd5e1; + cursor: not-allowed; + transform: none; + background: transparent; +} + +.workbench-ai-thread { + width: min(960px, 100%); + min-height: 0; + display: flex; + flex-direction: column; + gap: 20px; + margin: 0 auto; + overflow-y: auto; + padding: 64px 4px 30px; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.workbench-ai-thread > :first-child { + margin-top: auto; +} + +.workbench-ai-thread::-webkit-scrollbar { + width: 0; + height: 0; + display: none; +} + +.workbench-ai-empty-thread { + flex: 0 0 auto; + width: min(720px, 100%); + justify-self: center; + display: grid; + gap: 8px; + padding: 28px; + border: 1px dashed rgba(148, 163, 184, 0.28); + border-radius: 18px; + background: rgba(255, 255, 255, 0.48); + color: var(--ai-muted); + text-align: center; +} + +.workbench-ai-empty-thread strong { + color: var(--ai-ink); + font-size: 18px; + font-weight: 820; +} + +.workbench-ai-empty-thread p { + margin: 0; + font-size: 14px; + line-height: 1.65; +} + +.workbench-ai-message { + flex: 0 0 auto; + min-width: 0; + display: grid; + gap: 10px; + animation: workbenchAiControlIn 360ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; +} + +.workbench-ai-message.is-user { + justify-items: end; +} + +.workbench-ai-user-bubble { + max-width: min(680px, 82%); + padding: 12px 16px; + border-radius: 18px 18px 5px; + background: + linear-gradient(135deg, rgba(var(--ai-theme-rgb), 0.96), rgba(29, 78, 216, 0.9)); + color: #fff; + font-size: 15px; + font-weight: 620; + line-height: 1.6; + box-shadow: none; + overflow-wrap: anywhere; +} + +.workbench-ai-answer-card { + width: 100%; + padding: 34px 36px; + border: 1px solid rgba(226, 232, 240, 0.86); + border-radius: 20px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.99), rgba(255, 255, 255, 0.96)); + color: #111827; + box-shadow: none; + backdrop-filter: none; +} + +.workbench-ai-answer-card.pending { + color: var(--ai-muted); +} + +.workbench-ai-thinking-panel { + position: relative; + display: grid; + gap: 0; + margin-bottom: 22px; + border: 1px solid rgba(191, 219, 254, 0.58); + border-radius: 14px; + background: + linear-gradient(180deg, rgba(239, 246, 255, 0.82), rgba(248, 250, 252, 0.66)); + overflow: hidden; + transition: + border-color 180ms ease, + background 180ms ease; +} + +.workbench-ai-thinking-panel.is-expanded { + padding: 14px 44px 14px 16px; +} + +.workbench-ai-thinking-panel.is-collapsed { + padding: 0; +} + +.workbench-ai-thinking-toggle { + width: 100%; + min-height: 40px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 10px 10px 10px 14px; + border: 0; + border-radius: 14px; + background: transparent; + color: #1e3a8a; + cursor: pointer; + transition: + background 180ms ease; +} + +.workbench-ai-thinking-toggle:hover { + background: rgba(255, 255, 255, 0.36); +} + +.workbench-ai-thinking-expanded { + position: relative; + min-width: 0; +} + +.workbench-ai-thinking-collapse-btn { + position: absolute; + top: -4px; + right: -34px; + width: 28px; + height: 28px; + display: grid; + place-items: center; + padding: 0; + border: 0; + border-radius: 10px; + background: transparent; + color: #64748b; + cursor: pointer; +} + +.workbench-ai-thinking-collapse-btn:hover { + background: rgba(59, 130, 246, 0.08); + color: #1e3a8a; +} + +.workbench-ai-thinking-toggle-left { + min-width: 0; + display: inline-flex; + align-items: center; + gap: 10px; +} + +.workbench-ai-thinking-toggle strong { + color: #1e3a8a; + font-size: 13px; + font-weight: 860; + line-height: 1.35; +} + +.workbench-ai-thinking-toggle small { + color: #64748b; + font-size: 12px; + font-weight: 720; + line-height: 1.35; +} + +.workbench-ai-thinking-toggle > i { + color: #64748b; + font-size: 18px; + line-height: 1; +} + +.workbench-ai-thinking-list { + display: grid; + gap: 10px; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + overflow: visible; +} + +.workbench-ai-thinking-item { + display: grid; + grid-template-columns: 18px minmax(0, 1fr); + gap: 10px; + align-items: flex-start; + color: #64748b; +} + +.workbench-ai-thinking-dot { + justify-self: center; + width: 8px; + height: 8px; + margin-top: 7px; + border-radius: 50%; + background: #60a5fa; + box-shadow: 0 0 0 5px rgba(96, 165, 250, 0.16); +} + +.workbench-ai-thinking-item.is-running .workbench-ai-thinking-dot { + animation: workbenchAiPulse 1400ms ease-in-out infinite; +} + +.workbench-ai-thinking-toggle .workbench-ai-thinking-dot { + flex: 0 0 auto; + margin-top: 0; +} + +.workbench-ai-thinking-dot.running { + animation: workbenchAiPulse 1400ms ease-in-out infinite; +} + +.workbench-ai-thinking-item.is-failed .workbench-ai-thinking-dot { + background: #f97316; + box-shadow: 0 0 0 5px rgba(249, 115, 22, 0.14); +} + +.workbench-ai-thinking-item strong { + display: block; + color: #1e3a8a; + font-size: 13px; + font-weight: 850; + line-height: 1.5; +} + +.workbench-ai-thinking-item p { + margin: 2px 0 0; + color: #64748b; + font-size: 13px; + font-weight: 560; + line-height: 1.55; +} + +.workbench-ai-pending-line { + color: #64748b; + font-size: 15px; + font-weight: 620; + line-height: 1.7; +} + +.workbench-ai-answer-markdown { + color: #111827; + font-size: 16px; + font-weight: 500; + line-height: 1.86; + overflow-wrap: anywhere; +} + +.workbench-ai-answer-markdown :deep(*) { + letter-spacing: 0; +} + +.workbench-ai-answer-markdown :deep(h1), +.workbench-ai-answer-markdown :deep(h2), +.workbench-ai-answer-markdown :deep(h3), +.workbench-ai-answer-markdown :deep(h4) { + margin: 0 0 16px; + color: #0f172a; + font-weight: 900; + line-height: 1.35; +} + +.workbench-ai-answer-markdown :deep(h3) { + font-size: 21px; +} + +.workbench-ai-answer-markdown :deep(p) { + margin: 0; +} + +.workbench-ai-answer-markdown :deep(p + p) { + margin-top: 18px; +} + +.workbench-ai-answer-markdown :deep(ul), +.workbench-ai-answer-markdown :deep(ol) { + display: grid; + gap: 12px; + margin: 16px 0 0; + padding-left: 24px; +} + +.workbench-ai-answer-markdown :deep(li) { + padding-left: 4px; +} + +.workbench-ai-answer-markdown :deep(li::marker) { + color: #2563eb; + font-weight: 850; +} + +.workbench-ai-answer-markdown :deep(strong) { + color: #0f172a; + font-weight: 850; +} + +.workbench-ai-answer-markdown :deep(hr) { + margin: 26px 0; + border: 0; + border-top: 1px solid rgba(226, 232, 240, 0.9); +} + +.workbench-ai-answer-markdown :deep(blockquote) { + margin: 18px 0 0; + padding: 14px 16px; + border-left: 3px solid rgba(37, 99, 235, 0.5); + border-radius: 12px; + background: rgba(239, 246, 255, 0.62); + color: #475569; +} + +.workbench-ai-answer-markdown :deep(.markdown-table-wrap) { + overflow-x: auto; + margin-top: 18px; + border: 1px solid rgba(226, 232, 240, 0.9); + border-radius: 14px; +} + +.workbench-ai-answer-markdown :deep(table) { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +.workbench-ai-answer-markdown :deep(th), +.workbench-ai-answer-markdown :deep(td) { + padding: 11px 14px; + border-bottom: 1px solid rgba(226, 232, 240, 0.9); + text-align: left; +} + +.workbench-ai-answer-markdown :deep(th) { + background: rgba(248, 250, 252, 0.92); + color: #334155; + font-weight: 850; +} + +.workbench-ai-suggested-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 22px; + padding-top: 18px; + border-top: 1px solid rgba(226, 232, 240, 0.82); +} + +.workbench-ai-suggested-actions button { + min-height: 38px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 14px; + border: 1px solid rgba(37, 99, 235, 0.18); + border-radius: 999px; + background: rgba(239, 246, 255, 0.76); + color: #1d4ed8; + font: inherit; + font-size: 13px; + font-weight: 850; + cursor: pointer; + transition: + background 160ms ease, + transform 160ms ease; +} + +.workbench-ai-suggested-actions button:hover { + transform: translateY(-1px); + background: #eff6ff; +} + +.workbench-ai-message-actions { + display: flex; + align-items: center; + gap: 12px; + padding: 0 6px; + color: rgba(100, 116, 139, 0.72); +} + +.workbench-ai-message-actions button { + width: 28px; + height: 28px; + display: inline-grid; + place-items: center; + border: 0; + border-radius: 8px; + background: transparent; + color: inherit; + cursor: pointer; + transition: + background 160ms ease, + color 160ms ease, + transform 160ms ease; +} + +.workbench-ai-message-actions button:hover { + color: var(--ai-blue-deep); + background: rgba(15, 23, 42, 0.045); + transform: translateY(-1px); +} + +.workbench-ai-message-time { + color: rgba(148, 163, 184, 0.9); + font-size: 12px; + font-weight: 650; + padding: 0 6px; + white-space: nowrap; + margin-left: auto; +} + +.workbench-ai-conversation-bottom { + position: relative; + z-index: 6; + width: min(980px, 100%); + display: grid; + gap: 10px; + justify-self: center; + padding-top: 10px; +} + +.workbench-ai-conversation-bottom::before { + display: none; +} + +.workbench-ai-composer--inline { + width: 100%; + min-height: 126px; + grid-template-rows: minmax(54px, 1fr) auto; + margin: 0; + padding: 20px 24px; + border-color: rgba(226, 232, 240, 0.88); + border-radius: 18px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.94)); + box-shadow: none; + animation: workbenchAiControlIn 360ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) backwards; +} + +.workbench-ai-composer--inline textarea { + min-height: 54px; + font-size: 16px; +} + +.workbench-ai-file-strip.inline { + justify-content: flex-start; + animation-delay: 0ms; +} + +.workbench-ai-disclaimer { + margin: 0; + color: rgba(100, 116, 139, 0.58); + font-size: 12px; + font-weight: 600; + line-height: 1.4; + text-align: center; +} + +.workbench-ai-thinking-collapse-enter-active, +.workbench-ai-thinking-collapse-leave-active { + transition: + max-height 220ms ease, + opacity 180ms ease, + transform 180ms ease; +} + +.workbench-ai-thinking-collapse-enter-from, +.workbench-ai-thinking-collapse-leave-to { + max-height: 0; + opacity: 0; + transform: translateY(-4px); +} + +.workbench-ai-thinking-collapse-enter-to, +.workbench-ai-thinking-collapse-leave-from { + max-height: 460px; + opacity: 1; + transform: translateY(0); +} + +.workbench-ai-confirm-mask { + position: fixed; + inset: 0; + z-index: 60; + display: grid; + place-items: center; + padding: 24px; + background: rgba(15, 23, 42, 0.22); + backdrop-filter: blur(10px); +} + +.workbench-ai-confirm-dialog { + width: min(420px, 100%); + display: grid; + gap: 14px; + padding: 24px; + border: 1px solid rgba(226, 232, 240, 0.86); + border-radius: 18px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 251, 247, 0.96)); + color: #0f172a; + box-shadow: + 0 28px 80px rgba(15, 23, 42, 0.22), + inset 0 1px 0 rgba(255, 255, 255, 0.95); +} + +.workbench-ai-confirm-dialog h3 { + margin: 0; + color: #111827; + font-size: 18px; + font-weight: 860; + line-height: 1.35; +} + +.workbench-ai-confirm-dialog p { + margin: 0; + color: #64748b; + font-size: 14px; + font-weight: 540; + line-height: 1.7; +} + +.workbench-ai-confirm-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 2px; +} + +.workbench-ai-confirm-actions button { + min-height: 38px; + padding: 0 15px; + border: 0; + border-radius: 10px; + font: inherit; + font-size: 13px; + font-weight: 820; + cursor: pointer; +} + +.workbench-ai-confirm-actions .ghost { + background: rgba(241, 245, 249, 0.9); + color: #475569; +} + +.workbench-ai-confirm-actions .danger { + background: #dc2626; + color: #fff; + box-shadow: 0 12px 22px rgba(220, 38, 38, 0.2); +} + +.workbench-ai-confirm-fade-enter-active, +.workbench-ai-confirm-fade-leave-active { + transition: opacity 180ms ease; +} + +.workbench-ai-confirm-fade-enter-active .workbench-ai-confirm-dialog, +.workbench-ai-confirm-fade-leave-active .workbench-ai-confirm-dialog { + transition: + opacity 180ms ease, + transform 180ms ease; +} + +.workbench-ai-confirm-fade-enter-from, +.workbench-ai-confirm-fade-leave-to { + opacity: 0; +} + +.workbench-ai-confirm-fade-enter-from .workbench-ai-confirm-dialog, +.workbench-ai-confirm-fade-leave-to .workbench-ai-confirm-dialog { + opacity: 0; + transform: translateY(10px) scale(0.98); +} + +@keyframes workbenchAiControlIn { + from { + opacity: 0; + transform: translateY(18px) scale(0.985); + filter: blur(3px); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0); + } +} + +@keyframes workbenchAiPopoverIn { + from { + opacity: 0; + transform: translateY(8px) scale(0.98); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes workbenchAiPulse { + 0%, + 100% { + transform: scale(1); + opacity: 0.86; + } + + 50% { + transform: scale(1.22); + opacity: 1; + } +} + +@media (max-width: 960px) { + .workbench-ai-mode { + padding: 34px 20px 34px; + } + + .workbench-ai-mode::after { + opacity: 0.58; + } + + .workbench-ai-shell { + gap: 18px; + padding-top: 18px; + } + + .workbench-ai-copy h2 { + font-size: 28px; + } + + .workbench-ai-composer { + min-height: 168px; + padding: 24px 24px 20px; + border-radius: 22px; + } + + .workbench-ai-action-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + } + + .workbench-ai-conversation { + padding: 24px 22px 22px; + } + + .workbench-ai-answer-card { + padding: 26px; + } +} + +@media (max-width: 640px) { + .workbench-ai-mode { + padding: 24px 14px 28px; + } + + .workbench-ai-orb { + width: 88px; + height: 88px; + } + + .workbench-ai-orb__image { + width: 100%; + height: 100%; + } + + .workbench-ai-copy h2 { + font-size: 24px; + } + + .workbench-ai-copy p { + font-size: 14px; + } + + .workbench-ai-composer { + min-height: 158px; + padding: 20px 16px 16px; + } + + .workbench-ai-composer textarea { + font-size: 16px; + } + + .workbench-ai-composer-toolbar { + gap: 10px; + } + + .workbench-ai-tool-buttons { + gap: 8px; + } + + .workbench-ai-icon-btn { + width: 40px; + height: 40px; + } + + .workbench-ai-send-btn { + width: 40px; + height: 40px; + } + + .workbench-ai-count { + font-size: 13px; + } + + .workbench-ai-action-row { + grid-template-columns: 1fr; + } + + .workbench-ai-conversation { + padding: 18px 12px 16px; + } + + .workbench-ai-thread { + padding-bottom: 18px; + } + + .workbench-ai-answer-card { + padding: 20px; + border-radius: 16px; + } + + .workbench-ai-answer-card p { + font-size: 15px; + } + + .workbench-ai-user-bubble { + max-width: 92%; + } + + .workbench-ai-composer--inline { + min-height: 120px; + padding: 18px 16px; + } + + .workbench-ai-date-popover { + width: min(276px, calc(100vw - 36px)); + } +} + +@media (prefers-reduced-motion: reduce) { + .workbench-ai-orb, + .workbench-ai-copy, + .workbench-ai-composer, + .workbench-ai-composer textarea, + .workbench-ai-icon-btn, + .workbench-ai-send-btn, + .workbench-ai-file-strip, + .workbench-ai-action, + .workbench-ai-message, + .workbench-ai-composer--inline, + .workbench-ai-date-popover, + .workbench-ai-thinking-dot { + animation: none; + opacity: 1; + transform: none; + filter: none; + } + + .workbench-ai-panel-swap-enter-active, + .workbench-ai-panel-swap-leave-active, + .workbench-ai-thinking-collapse-enter-active, + .workbench-ai-thinking-collapse-leave-active, + .workbench-ai-confirm-fade-enter-active, + .workbench-ai-confirm-fade-leave-active, + .workbench-ai-confirm-fade-enter-active .workbench-ai-confirm-dialog, + .workbench-ai-confirm-fade-leave-active .workbench-ai-confirm-dialog { + transition: none; + } +} diff --git a/web/src/assets/styles/components/personal-workbench-responsive.css b/web/src/assets/styles/components/personal-workbench-responsive.css index 0d3f17a..769bc1c 100644 --- a/web/src/assets/styles/components/personal-workbench-responsive.css +++ b/web/src/assets/styles/components/personal-workbench-responsive.css @@ -1,77 +1,40 @@ -/* 1080p / 小高度屏:进一步压缩 AI 助手卡片高度 (排除手机端) */ +/* 1080p / 小高度屏:让传统模式顶部趋势卡更紧凑 */ @media (max-height: 980px) and (min-width: 761px) { .workbench { - --hero-padding-top: 20px; - --hero-padding-bottom: 20px; - --hero-title-size: 28px; - --hero-copy-gap: 16px; - --hero-title-bottom-gap: 10px; - --composer-min-height: 108px; - --composer-textarea-height: 48px; - --composer-padding-block: 10px; - --quick-prompts-gap-top: 8px; - --capability-row-height: 96px; + --hero-title-size: 31px; + --trend-card-min-height: 232px; + --capability-row-height: 106px; gap: 9px; } - .assistant-hero { - --assistant-bg-position: right center; - --assistant-decor-width: clamp(760px, 66vw, 980px); - --assistant-decor-opacity: 0.86; - padding: var(--hero-padding-top) 18px var(--hero-padding-bottom) 44px; + .workbench-trend-hero { + padding: 24px 20px 10px 20px; } - .assistant-copy p { - font-size: 14px; - line-height: 1.5; - margin-bottom: 0; + .workbench-trend-card { } - .assistant-composer textarea { - font-size: 15px; - } - - .composer-icon-button, - .composer-send-button { - height: 32px; - } - - .composer-send-button { - width: 50px; + .trend-chart-panel { + min-height: 128px; } } /* 2K 宽屏但内容区仍偏高时,略收紧(避免 hero 独占过多纵向空间) */ @media (min-width: 1920px) and (max-height: 1100px) { .workbench { - --hero-padding-top: 22px; - --hero-padding-bottom: 22px; - --hero-title-size: 29px; - --composer-min-height: 114px; - --composer-textarea-height: 50px; - --capability-row-height: 100px; + --hero-title-size: 32px; + --trend-card-min-height: 236px; + --capability-row-height: 108px; } } @media (max-width: 1440px) { .workbench { - grid-template-rows: auto var(--capability-row-height) minmax(0, 1fr); gap: 10px; } - .assistant-hero { - --assistant-bg-position: right center; - --assistant-decor-width: clamp(760px, 66vw, 980px); - --assistant-decor-opacity: 0.9; - padding: var(--hero-padding-top) 18px var(--hero-padding-bottom) 44px; - } - - .assistant-copy { - width: min(940px, 92%); - } - - .assistant-copy h1 { - font-size: 33px; + .trend-summary-panel h1 { + font-size: 32px; } .capability-grid--privileged { @@ -83,7 +46,7 @@ } .capability-card { - padding: 17px 12px 17px 22px; + padding: 18px 14px 18px 22px; } .capability-copy { @@ -109,24 +72,15 @@ .workbench { height: auto; min-height: 100%; - grid-template-rows: auto auto auto; gap: 12px; } - .assistant-hero { - --assistant-bg-position: right center; - --assistant-decor-width: clamp(620px, 74vw, 860px); - --assistant-decor-opacity: 0.62; - --assistant-readability-mask: - linear-gradient(90deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0.5) 58%, rgba(255, 255, 255, 0.06) 100%); - --assistant-theme-tint: - linear-gradient(135deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.12) 0%, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.04) 58%, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.09) 100%); - backdrop-filter: blur(10px) saturate(1.12); - -webkit-backdrop-filter: blur(10px) saturate(1.12); + .workbench-trend-card { + grid-template-columns: 1fr; } - .assistant-copy { - width: min(820px, 92%); + .trend-summary-panel { + align-content: start; } .capability-grid--privileged { @@ -149,126 +103,91 @@ @media (min-width: 961px) and (max-width: 1440px), (min-width: 961px) and (max-height: 820px) { .workbench { - --hero-padding-top: 14px; - --hero-padding-bottom: 14px; - --hero-title-size: 24px; - --hero-copy-gap: 14px; - --hero-title-bottom-gap: 8px; - --composer-min-height: 92px; - --composer-textarea-height: 38px; - --composer-padding-block: 8px; - --quick-prompts-gap-top: 5px; - --capability-row-height: 82px; + --hero-title-size: 30px; + --trend-card-min-height: 232px; + --capability-row-height: 102px; gap: 8px; } - .assistant-hero { - --assistant-decor-width: clamp(680px, 60vw, 880px); - --assistant-decor-opacity: 0.72; - padding: var(--hero-padding-top) 16px var(--hero-padding-bottom) 34px; + .workbench-trend-hero { + padding: 24px 18px 10px 18px; } - .assistant-copy { - width: min(900px, 92%); + .workbench-trend-card { + min-height: 0; } - .assistant-copy h1 { - margin-bottom: var(--hero-title-bottom-gap); + .trend-summary-panel { + gap: 7px; + } + + .trend-summary-panel h1 { + margin-bottom: 28px; font-size: var(--hero-title-size); line-height: 1.14; } - .assistant-composer { - min-height: var(--composer-min-height); - gap: 4px; - padding: var(--composer-padding-block) 14px 8px; + .trend-total { + font-size: 42px; } - .assistant-composer textarea { - height: var(--composer-textarea-height); - min-height: var(--composer-textarea-height); - max-height: var(--composer-textarea-height); + .trend-summary-panel small { + display: none; + } + + .trend-chart-panel { + min-height: 0; + } + + .trend-chart-head strong { font-size: 14px; - line-height: 1.42; } - .composer-toolbar { - gap: 8px; - } - - .composer-icon-button, - .composer-send-button { - height: 30px; - } - - .composer-icon-button { - width: 30px; - font-size: 17px; - } - - .composer-send-button { - width: 46px; - font-size: 16px; - } - - .composer-count { - font-size: 12px; - } - - .quick-prompts { - gap: 8px; - margin-top: var(--quick-prompts-gap-top); + .trend-chart-source { font-size: 12.5px; } - .quick-prompts button { - min-height: 24px; - padding: 0 10px; - font-size: 12px; - } - .capability-grid { gap: 10px; } .capability-card { - grid-template-columns: 34px minmax(0, 1fr) 14px; - gap: 10px; - padding: 12px 12px 12px 16px; + grid-template-columns: 40px minmax(0, 1fr) 16px; + gap: 12px; + padding: 15px 14px 15px 18px; } .capability-icon { - --workbench-list-icon-size: 34px; - --workbench-list-icon-art-size: 20px; - width: 34px; - height: 34px; + --workbench-list-icon-size: 40px; + --workbench-list-icon-art-size: 24px; + width: 40px; + height: 40px; } .capability-copy { - gap: 2px; + gap: 3px; } .capability-copy strong { - font-size: 13px; + font-size: 14px; line-height: 1.2; } .capability-copy small { - font-size: 11px; + font-size: 12px; line-height: 1.22; } .capability-arrow { - width: 14px; - min-width: 14px; - font-size: 16px; + width: 16px; + min-width: 16px; + font-size: 17px; } } @media (max-width: 760px) { .workbench { height: auto; - grid-template-rows: none; gap: 14px; overflow: visible; --workbench-glass-base: @@ -279,47 +198,36 @@ --workbench-glass-blur: blur(14px) saturate(1.2); } - .assistant-hero { - min-height: auto; - --assistant-bg-position: right center; - --assistant-decor-width: min(620px, 118vw); - --assistant-decor-opacity: 0.36; - --assistant-readability-mask: - linear-gradient(180deg, rgba(255, 255, 255, 0.86) 0%, rgba(255, 255, 255, 0.76) 100%), - linear-gradient(90deg, rgba(255, 255, 255, 0.88) 0%, rgba(255, 255, 255, 0.52) 100%); - --assistant-theme-tint: - linear-gradient(135deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.12) 0%, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.04) 100%); - padding: 24px 18px 24px; - backdrop-filter: blur(9px) saturate(1.1); - -webkit-backdrop-filter: blur(9px) saturate(1.1); + .workbench-trend-hero { + padding: 16px; } - .assistant-copy { - width: 100%; + .workbench-trend-card { + grid-template-columns: 1fr; + gap: 18px; } - .assistant-copy h1 { + .trend-summary-panel h1 { max-width: 320px; font-size: 28px; } - .assistant-composer { - padding: 14px; + .trend-summary-panel { + transform: none; } - .composer-toolbar { + .trend-total { + font-size: 34px; + } + + .trend-chart-head { + align-items: flex-start; + flex-direction: column; gap: 8px; - flex-wrap: wrap; } - .composer-count { - order: 4; - width: 100%; - margin-left: 0; - } - - .composer-send-button { - margin-left: auto; + .trend-chart-panel { + min-height: 148px; } .capability-grid, @@ -356,88 +264,33 @@ } } -/* 针对低高度视口(如低于 840px,包含大部分笔记本 768px 高度),解除 height: 100% 限制,让内容流式高度,防止纵向元素被过度压扁 (排除手机端) */ +/* 针对低高度视口,解除 height: 100% 限制,防止纵向元素被过度压扁 */ @media (max-height: 840px) and (min-width: 761px) { .workbench { height: auto; min-height: 100%; - grid-template-rows: auto var(--capability-row-height) auto; } } -/* 手机端/窄屏自适应优化 (560px 以下) */ @media (max-width: 560px) { - /* 常用提问横向滑动展示,避免折行过多撑爆高度 */ - .quick-prompts { - display: flex; - flex-wrap: nowrap; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - width: 100%; - gap: 8px; - padding-bottom: 2px; - } - - .quick-prompts span { - display: none; /* 隐藏“常用提问:”前缀,以最大化利用横向空间 */ - } - - .quick-prompts button { - flex-shrink: 0; - padding: 0 10px; - min-height: 26px; - font-size: 12px; - } - - /* 隐藏常用提问横滑条的原生滚动条,保持精致视觉 */ - .quick-prompts::-webkit-scrollbar { - display: none; - } - - .assistant-hero { - --assistant-bg-position: 72% center; - padding: 20px 14px 20px; + .workbench-trend-hero { + padding: 14px; } } -/* 手机端/窄屏自适应优化 (480px 以下) */ @media (max-width: 480px) { - /* 输入框更小巧 */ - .assistant-composer { - padding: 10px 12px; - min-height: 94px; + .trend-summary-panel h1 { + font-size: 24px; } - .assistant-composer textarea { - font-size: 14px; - height: 42px; - min-height: 42px; + .trend-total { + font-size: 30px; } - .composer-toolbar { - gap: 6px; + .trend-chart-panel { + min-height: 132px; } - .composer-icon-button, - .composer-send-button { - height: 30px; - font-size: 13px; - } - - .composer-icon-button { - width: 30px; - } - - .composer-send-button { - width: 46px; - } - - /* 限制上传的附件文件芯片的最大宽度,防止溢出 */ - .assistant-file-chip { - max-width: 110px; - } - - /* AI 财务助手卡片尺寸更精致 */ .capability-card { padding: 12px 10px 12px 14px; gap: 8px; @@ -463,7 +316,6 @@ font-size: 11px; } - /* 重点优化:费用进度行的网格区域(Grid Area)双行重构 */ .progress-row { display: grid; grid-template-columns: minmax(70px, auto) 1fr minmax(74px, auto); @@ -506,7 +358,7 @@ .progress-result { grid-area: result; width: 100%; - justify-items: end; /* 金额和状态右对齐 */ + justify-items: end; gap: 2px; } @@ -515,9 +367,9 @@ } .progress-status { - font-size: 11px; min-height: 18px; padding: 0 5px; + font-size: 11px; } .progress-steps { @@ -526,7 +378,6 @@ margin-top: 4px; } - /* 缩小步骤图图标与连线 */ .progress-step i { width: 14px; height: 14px; @@ -541,7 +392,6 @@ top: 7px; } - /* 侧边分析栏优化 */ .side-panel { padding: 8px 10px; gap: 4px; diff --git a/web/src/assets/styles/components/personal-workbench.css b/web/src/assets/styles/components/personal-workbench.css index 66e6de8..487fb6c 100644 --- a/web/src/assets/styles/components/personal-workbench.css +++ b/web/src/assets/styles/components/personal-workbench.css @@ -1,14 +1,11 @@ .workbench { --hero-padding-top: 26px; --hero-padding-bottom: 26px; - --hero-title-size: 30px; + --hero-title-size: 34px; --hero-copy-gap: 6px; --hero-title-bottom-gap: 18px; - --composer-min-height: 122px; - --composer-textarea-height: 54px; - --composer-padding-block: 12px; - --quick-prompts-gap-top: 10px; - --capability-row-height: 104px; + --trend-card-min-height: 260px; + --capability-row-height: 116px; --workbench-ink: var(--ink, #1e293b); --workbench-text: var(--text, #334155); --workbench-muted: var(--muted, #64748b); @@ -30,8 +27,8 @@ margin: 0 auto; height: 100%; min-width: 0; - display: grid; - grid-template-rows: auto var(--capability-row-height) minmax(0, 1fr); + display: flex; + flex-direction: column; gap: 10px; overflow: visible; color: var(--workbench-ink); @@ -41,7 +38,7 @@ background-color: var(--workbench-surface-soft); } -.workbench :where(button, textarea) { +.workbench :where(button) { font: inherit; } @@ -58,338 +55,139 @@ .workbench :where(button:disabled) { cursor: not-allowed; opacity: 0.7; } -.assistant-hero { - --assistant-bg-position: right center; - --assistant-decor-width: clamp(860px, 62vw, 1180px); - --assistant-decor-opacity: 0.92; - --assistant-readability-mask: - linear-gradient(90deg, rgba(255, 255, 255, 0.74) 0%, rgba(255, 255, 255, 0.34) 46%, rgba(255, 255, 255, 0) 100%); - --assistant-theme-tint: - linear-gradient(135deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.11), rgba(var(--theme-primary-rgb, 58, 124, 165), 0.025) 54%, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.075)); +.workbench-trend-hero { position: relative; z-index: 2; + flex: 0 0 var(--trend-card-min-height); + height: var(--trend-card-min-height); min-height: 0; - display: flex; - flex-direction: column; - justify-content: center; - overflow: visible; - padding: var(--hero-padding-top) 20px var(--hero-padding-bottom) 52px; - border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.18); - border-radius: 4px; + padding: 24px 28px; + overflow: hidden; + border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.16); + border-radius: 12px; background: - linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.54)), - var(--assistant-theme-tint); - background-color: rgba(247, 252, 255, 0.72); - backdrop-filter: blur(14px) saturate(1.18); - -webkit-backdrop-filter: blur(14px) saturate(1.18); + linear-gradient(120deg, rgba(255, 255, 255, 0.85), rgba(249, 252, 255, 0.7)), + linear-gradient(135deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.08), transparent 68%); + backdrop-filter: blur(12px) saturate(140%); + -webkit-backdrop-filter: blur(12px) saturate(140%); box-shadow: - 0 12px 28px rgba(15, 23, 42, 0.045), - inset 0 1px 0 rgba(255, 255, 255, 0.86), - inset 0 -1px 0 rgba(var(--theme-primary-rgb, 58, 124, 165), 0.07); - isolation: isolate; - animation: workbenchItemIn 560ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) both; + 0 16px 32px rgba(15, 23, 42, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.94); + animation: workbenchItemIn 520ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) both; animation-delay: 0ms; } -.assistant-hero::after { - content: ""; - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 82%; - min-width: 760px; - background: url("../../images/workbench-hero-right-bg.png") var(--assistant-bg-position) / var(--assistant-decor-width) auto no-repeat; - opacity: var(--assistant-decor-opacity); - pointer-events: none; - z-index: 0; -} - -.assistant-hero::before { - content: ""; - position: absolute; - inset: 0; - border-radius: inherit; - background: - var(--assistant-readability-mask), - linear-gradient(120deg, rgba(255, 255, 255, 0.36), transparent 22%, transparent 72%, rgba(255, 255, 255, 0.18)), - linear-gradient(135deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.05), transparent 58%); - pointer-events: none; - z-index: 1; -} - -.assistant-copy { +.workbench-trend-card { position: relative; - z-index: 3; - width: min(980px, 94%); + z-index: 1; display: grid; - gap: var(--hero-copy-gap); + grid-template-columns: minmax(200px, 0.28fr) minmax(0, 1fr); + align-items: stretch; + gap: 16px; + width: 100%; + height: 100%; + min-height: 0; + padding: 0; + overflow: hidden; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; } -.assistant-copy h1 { - margin: 0 0 var(--hero-title-bottom-gap); +.trend-summary-panel { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.trend-summary-panel h1 { + margin: 0 0 44px 0; color: var(--workbench-ink); font-size: var(--hero-title-size); - line-height: 1.18; + line-height: 1.16; + font-weight: 880; +} + +.trend-summary-panel p { + margin: 0 0 4px; + color: var(--workbench-muted); + font-size: 14px; + font-weight: 650; +} + +.trend-total { + background: linear-gradient(110deg, var(--workbench-ink) 20%, var(--workbench-primary-active) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: var(--workbench-ink); + font-size: clamp(38px, 3.3vw, 54px); + line-height: 1; + font-weight: 860; + letter-spacing: -0.5px; + filter: drop-shadow(0 2px 8px rgba(var(--theme-primary-rgb, 58, 124, 165), 0.12)); +} + +.trend-change { + display: inline-flex; + align-items: center; + gap: 5px; + min-height: 26px; + color: var(--workbench-primary-active); + font-size: 13px; + font-weight: 800; +} + +.trend-change.is-down { + color: #b45309; +} + +.trend-summary-panel small { + color: color-mix(in srgb, var(--workbench-muted) 80%, #ffffff); + font-size: 12px; + font-weight: 650; +} + +.trend-chart-panel { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + align-content: stretch; + gap: 8px; + min-width: 0; + min-height: 0; +} + +.trend-chart-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + min-width: 0; + color: var(--workbench-ink); +} + +.trend-chart-head strong { + font-size: 15px; font-weight: 850; } -.assistant-copy h1 span:not(.typing-cursor) { - color: var(--workbench-primary-active); - display: inline-block; - animation: workbenchItemIn 400ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) both; -} - -.typing-cursor { - display: inline-block; - color: var(--workbench-primary-active); - font-weight: 400; - margin-left: 2px; - animation: cursorBlink 0.9s step-end infinite; -} - -@keyframes cursorBlink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} - -.assistant-copy p { - max-width: 680px; - margin: 0 0 2px; +.trend-chart-source { color: var(--workbench-muted); - font-size: 15px; - line-height: 1.6; - font-weight: 600; -} - -.assistant-copy > * { - animation: workbenchItemIn 480ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)) both; -} - -.assistant-copy > h1 { animation-delay: 80ms; } -.assistant-copy > p { animation-delay: 160ms; } -.assistant-copy > .assistant-composer { animation-delay: 240ms; } -.assistant-copy > .assistant-file-strip { animation-delay: 320ms; } -.assistant-copy > .quick-prompts { animation-delay: 320ms; } - -.assistant-file-input { display: none; } - -.assistant-composer { - position: relative; - z-index: 20; - display: grid; - gap: 6px; - max-width: 920px; - min-height: var(--composer-min-height); - padding: var(--composer-padding-block) 18px 10px; - border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.24); - border-radius: 4px; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.74)), - linear-gradient(135deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.045), rgba(255, 255, 255, 0.18)); - box-shadow: - 0 10px 24px rgba(15, 23, 42, 0.045), - inset 0 1px 0 rgba(255, 255, 255, 0.9), - inset 0 -1px 0 rgba(var(--theme-primary-rgb, 58, 124, 165), 0.06); - backdrop-filter: blur(10px) saturate(1.14); - -webkit-backdrop-filter: blur(10px) saturate(1.14); - transition: - border-color 180ms var(--ease), - background 180ms var(--ease), - box-shadow 180ms var(--ease); -} - -.assistant-composer::before { - content: ""; - position: absolute; - inset: 0; - z-index: 0; - border-radius: inherit; - background: - linear-gradient(110deg, rgba(255, 255, 255, 0.32), transparent 32%), - linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 42%); - pointer-events: none; -} - -.assistant-composer > * { - position: relative; - z-index: 1; -} - -.assistant-composer:focus-within { - border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.58); - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.78)), - linear-gradient(135deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.06), rgba(255, 255, 255, 0.22)); - box-shadow: - 0 0 0 3px rgba(var(--theme-primary-rgb, 58, 124, 165), 0.11), - 0 14px 30px rgba(15, 23, 42, 0.055), - inset 0 1px 0 rgba(255, 255, 255, 0.94), - inset 0 -1px 0 rgba(var(--theme-primary-rgb, 58, 124, 165), 0.08); -} - -.assistant-composer textarea { - width: 100%; - min-width: 0; - height: var(--composer-textarea-height); - min-height: var(--composer-textarea-height); - max-height: var(--composer-textarea-height); - resize: none; - border: 0; - padding: 0; - background: transparent; - color: var(--workbench-ink); - font-size: 16px; - line-height: 1.55; - overflow: hidden; -} - -.assistant-composer textarea::placeholder { - color: color-mix(in srgb, var(--workbench-muted) 70%, #ffffff); -} - -.assistant-composer textarea:focus { outline: none; } - -.assistant-composer textarea[readonly] { - color: color-mix(in srgb, var(--workbench-ink) 72%, #ffffff); - cursor: progress; -} - -.assistant-intent-status { - display: inline-flex; - align-items: center; - width: fit-content; - max-width: 100%; - min-height: 28px; - gap: 8px; - padding: 0 10px; - border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.22); - border-radius: 4px; - background: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.08); - color: var(--workbench-primary-active); - font-size: 12px; - font-weight: 750; - line-height: 1.35; -} - -.assistant-intent-status i { - font-size: 15px; -} - -.composer-toolbar { - display: flex; - align-items: center; - gap: 12px; -} - -.composer-icon-button, -.composer-send-button { - display: inline-flex; - align-items: center; - justify-content: center; - height: 36px; - border-radius: 4px; - white-space: nowrap; -} - -.composer-icon-button { - width: 36px; - border: 1px solid var(--workbench-line); - background: var(--workbench-surface); - color: var(--workbench-text); - font-size: 19px; -} - -.composer-count { - margin-left: auto; - color: color-mix(in srgb, var(--workbench-muted) 75%, #ffffff); font-size: 13px; - font-weight: 650; -} - -.composer-send-button { - width: 56px; - background: var(--workbench-primary-active); - color: #fff; - font-size: 18px; - box-shadow: 0 6px 14px rgba(var(--theme-primary-rgb, 58, 124, 165), 0.16); -} - -.assistant-file-strip { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.assistant-file-note, -.assistant-file-chip { - display: inline-flex; - align-items: center; - max-width: 220px; - min-height: 28px; - padding: 0 10px; - border-radius: 4px; - font-size: 12px; - font-weight: 750; -} - -.assistant-file-note { - background: var(--workbench-primary-soft); - color: var(--workbench-primary-active); -} - -.assistant-file-chip { - overflow: hidden; - border: 1px solid var(--workbench-line); - background: var(--workbench-surface); - color: var(--workbench-text); - text-overflow: ellipsis; - white-space: nowrap; -} - -.assistant-file-clear { - color: var(--workbench-muted); - font-size: 12px; - font-weight: 750; -} - -.quick-prompts { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; - margin-top: var(--quick-prompts-gap-top); - margin-bottom: 0; - color: var(--workbench-text); - font-size: 14px; font-weight: 700; + white-space: nowrap; } -.quick-prompts button { - min-height: 28px; - padding: 0 14px; - border: 1px solid var(--workbench-line); - border-radius: 4px; - background: rgba(255, 255, 255, 0.86); - color: var(--workbench-text); - font-size: 13px; - font-weight: 650; -} - -.quick-prompts .quick-more { - display: inline-flex; - align-items: center; - gap: 4px; - border-color: transparent; - background: transparent; - color: var(--workbench-primary-active); - font-weight: 800; +.workbench-trend-chart { + min-height: 0; } .capability-grid { position: relative; z-index: 1; + flex: 0 0 var(--capability-row-height); display: grid; gap: 16px; min-height: 0; @@ -407,11 +205,11 @@ position: relative; isolation: isolate; display: grid; - grid-template-columns: 40px minmax(0, 1fr) 18px; + grid-template-columns: 44px minmax(0, 1fr) 18px; align-items: center; - gap: 14px; + gap: 16px; min-height: 0; - padding: 16px 18px 16px 22px; + padding: 18px 20px 18px 24px; overflow: visible; text-align: left; border: 1px solid rgba(255, 255, 255, 0.9); @@ -450,10 +248,10 @@ } .capability-icon { - --workbench-list-icon-size: 40px; - --workbench-list-icon-art-size: 24px; - width: 40px; - height: 40px; + --workbench-list-icon-size: 44px; + --workbench-list-icon-art-size: 26px; + width: 44px; + height: 44px; color: var(--capability-color); } @@ -467,7 +265,7 @@ .capability-copy strong { color: var(--workbench-ink); - font-size: 14px; + font-size: 15px; font-weight: 850; line-height: 1.25; overflow: hidden; @@ -479,7 +277,7 @@ .capability-copy small { overflow: hidden; color: var(--workbench-muted); - font-size: 12px; + font-size: 12.5px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; @@ -529,6 +327,7 @@ } .workbench-content-grid { + flex: 1 1 auto; display: grid; grid-template-columns: minmax(640px, 1.8fr) minmax(260px, 0.55fr); gap: 14px; @@ -1034,9 +833,7 @@ } .capability-card:hover, -.progress-row:hover, -.quick-prompts button:hover, -.composer-icon-button:hover { +.progress-row:hover { border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.24); color: var(--workbench-primary-active); } @@ -1053,9 +850,10 @@ } @media (prefers-reduced-motion: reduce) { - .assistant-hero, + .workbench-trend-hero, .capability-card, .workbench-card { animation: none !important; } } + diff --git a/web/src/assets/styles/components/sidebar-rail.css b/web/src/assets/styles/components/sidebar-rail.css index 21c38ce..f4e84fb 100644 --- a/web/src/assets/styles/components/sidebar-rail.css +++ b/web/src/assets/styles/components/sidebar-rail.css @@ -276,51 +276,43 @@ } .rail-user { - position: relative; + box-sizing: border-box; min-width: 0; - min-height: 78px; - margin: 0; - padding: 16px 20px 18px; - border-top: 1px solid #edf2f7; - transition: padding var(--rail-motion-duration) var(--rail-motion-ease); -} - -.user-summary { - position: relative; - min-width: 0; - min-height: 42px; - display: flex; + height: 72px; + min-height: 72px; + display: grid; + grid-template-columns: 42px minmax(0, 1fr) 44px; align-items: center; - gap: 10px; - padding: 4px; - color: #64748b; - border-radius: 4px; - cursor: pointer; + gap: 12px; + margin: 0; + padding: 12px 14px 12px 18px; + border-top: 1px solid rgba(203, 213, 225, 0.55); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.76), rgba(247, 250, 252, 0.9)), + rgba(255, 255, 255, 0.72); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84); transition: - gap var(--rail-motion-duration) var(--rail-motion-ease), - padding var(--rail-motion-duration) var(--rail-motion-ease), - background 180ms var(--ease); -} - -.rail-user:hover .user-summary { - background: rgba(255, 255, 255, 0.72); + grid-template-columns var(--rail-motion-duration) var(--rail-motion-ease), + padding var(--rail-motion-duration) var(--rail-motion-ease); } .user-avatar { - flex: 0 0 36px; - width: 36px; - height: 36px; + width: 42px; + height: 42px; display: grid; place-items: center; - border: 2px solid #fff; - border-radius: 999px; - background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-active)); - box-shadow: 0 6px 14px rgba(var(--theme-primary-rgb, 58, 124, 165), 0.18); + border: 2px solid rgba(255, 255, 255, 0.92); + border-radius: 50%; + background: + radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.22), transparent 32%), + linear-gradient(135deg, #1f4f96, #2f8d7b); + box-shadow: + 0 8px 16px rgba(45, 114, 217, 0.13), + inset 0 -1px 0 rgba(15, 23, 42, 0.08); color: #fff; - font-size: 14px; - font-weight: 800; + font-size: 15px; + font-weight: 820; transition: - flex-basis var(--rail-motion-duration) var(--rail-motion-ease), width var(--rail-motion-duration) var(--rail-motion-ease), height var(--rail-motion-duration) var(--rail-motion-ease); } @@ -328,9 +320,7 @@ .user-copy { flex: 1; min-width: 0; - max-width: 116px; - display: flex; - flex-direction: column; + display: grid; gap: 2px; opacity: 1; transition: @@ -341,9 +331,10 @@ } .user-copy strong { - color: #334155; - font-size: 14px; - font-weight: 750; + color: #182237; + font-size: 13px; + font-weight: 760; + line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -352,57 +343,47 @@ .user-copy span { color: #64748b; font-size: 12px; + font-weight: 520; + line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.user-summary .mdi { - flex: 0 0 18px; - font-size: 18px; - transition: - max-width var(--rail-motion-duration) var(--rail-motion-ease), - opacity var(--rail-fade-duration) var(--rail-motion-ease) var(--rail-label-delay); - will-change: max-width, opacity; +.user-actions { + display: grid; + grid-template-columns: 44px; + justify-content: end; } -.user-menu { - position: absolute; - right: 20px; - bottom: calc(100% - 6px); - min-width: 132px; - padding: 8px; - border: 1px solid rgba(226, 232, 240, 0.96); - border-radius: 4px; - background: rgba(255, 255, 255, 0.98); - box-shadow: 0 16px 32px rgba(15, 23, 42, 0.1); - opacity: 0; - transform: translateY(8px); - pointer-events: none; - transition: all 180ms var(--ease); - z-index: 4; -} - -.rail-user:hover .user-menu { - opacity: 1; - transform: translateY(0); - pointer-events: auto; -} - -.user-menu-item { +.user-action { width: 100%; - height: 38px; - display: flex; + min-width: 0; + height: 44px; + display: grid; align-items: center; - gap: 8px; - padding: 0 12px; - border: 0; - border-radius: 4px; + justify-content: center; + padding: 0; + border: 1px solid transparent; + border-radius: 10px; background: transparent; + color: #64748b; + cursor: pointer; + transition: + background 180ms var(--ease), + border-color 180ms var(--ease), + color 180ms var(--ease); +} + +.user-action:hover { + border-color: rgba(148, 163, 184, 0.28); + background: rgba(255, 255, 255, 0.78); color: #dc2626; - font-size: 13px; - font-weight: 700; - transition: all 180ms var(--ease); +} + +.user-action i { + font-size: 20px; + line-height: 1; } /* ========================================= */ @@ -489,33 +470,14 @@ } .rail-collapsed .rail-user { - position: relative; - z-index: 6; - padding: 14px 8px; - overflow: visible; -} - -.rail-collapsed .user-summary { + grid-template-columns: 42px; justify-content: center; - padding: 4px; - gap: 0; + padding: 14px 8px; } -.rail-user-menu-floating { - position: fixed; - z-index: 12000; - min-width: 132px; - padding: 8px; - border: 1px solid rgba(226, 232, 240, 0.96); - border-radius: 4px; - background: rgba(255, 255, 255, 0.98); - box-shadow: 0 16px 32px rgba(15, 23, 42, 0.14); - transform: translateY(-50%); - animation: railUserMenuIn 180ms var(--rail-motion-ease) both; -} - -.rail-user-menu-floating .user-menu-item { - width: 100%; +.rail-collapsed .user-copy, +.rail-collapsed .user-actions { + display: none; } :global(.rail-tooltip-popper) { diff --git a/web/src/assets/styles/components/stage-risk-advice-card.css b/web/src/assets/styles/components/stage-risk-advice-card.css index fd89b0a..024481d 100644 --- a/web/src/assets/styles/components/stage-risk-advice-card.css +++ b/web/src/assets/styles/components/stage-risk-advice-card.css @@ -1,7 +1,7 @@ .employee-risk-profile-card { display: grid; - gap: 10px; - padding: 12px 14px; + gap: 12px; + padding: 14px 16px; } .employee-risk-head { @@ -74,28 +74,28 @@ .employee-risk-body { display: grid; - gap: 10px; + gap: 12px; } .employee-risk-decision-panel { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(220px, 32%); + grid-template-columns: minmax(0, 1.15fr) minmax(220px, .85fr); align-items: stretch; gap: 12px; padding: 12px; - border: 1px solid #e2e8f0; - border-radius: 4px; - background: #f8fafc; + border: 1px solid #e5e7eb; + border-radius: 2px; + background: #ffffff; } .employee-risk-decision-panel.medium { - border-color: #fed7aa; - background: #fff7ed; + border-color: #f3e8d9; + background: #fffcf7; } .employee-risk-decision-panel.high { border-color: #fecaca; - background: #fef2f2; + background: #fff7f7; } .employee-risk-decision-main { @@ -110,13 +110,15 @@ font-size: 10px; font-weight: 850; line-height: 1.5; + letter-spacing: .03em; + text-transform: uppercase; } .employee-risk-decision-main strong { min-width: 0; color: #0f172a; - font-size: 13px; - font-weight: 850; + font-size: 15px; + font-weight: 900; overflow-wrap: anywhere; } @@ -143,8 +145,8 @@ justify-content: center; gap: 5px; padding: 10px 12px; - border: 1px solid #e2e8f0; - border-radius: 4px; + border: 1px solid #e5e7eb; + border-radius: 2px; background: #fff; } @@ -152,8 +154,8 @@ min-width: 0; color: #0f172a; font-size: 12px; - font-weight: 800; - line-height: 1.5; + font-weight: 900; + line-height: 1.45; overflow-wrap: anywhere; } @@ -165,12 +167,75 @@ color: #b91c1c; } +.employee-risk-decision-action p { + margin: 0; + color: #475569; + font-size: 12px; + line-height: 1.5; +} + +.employee-risk-review-summary { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0; +} + +.employee-risk-review-item { + min-width: 0; + flex: 1 1 180px; + display: grid; + gap: 4px; + padding: 9px 10px; + border: 1px solid #e5e7eb; + border-radius: 2px; + background: #fff; +} + +.employee-risk-review-item.medium { + border-color: #f3e8d9; + background: #fffcf7; +} + +.employee-risk-review-item.high { + border-color: #fecaca; + background: #fff7f7; +} + +.employee-risk-review-item dt, +.employee-risk-review-item dd { + margin: 0; +} + +.employee-risk-review-item dt { + color: #64748b; + font-size: 10px; + font-weight: 850; + line-height: 1.4; +} + +.employee-risk-review-item dd { + color: #334155; + font-size: 12px; + font-weight: 700; + line-height: 1.5; + overflow-wrap: anywhere; +} + +.employee-risk-review-item.high dd { + color: #991b1b; +} + +.employee-risk-review-item.medium dd { + color: #9a3412; +} + .employee-risk-profile-section { display: grid; gap: 8px; padding: 10px 12px; - border: 1px solid #e2e8f0; - border-radius: 4px; + border: 1px solid #e5e7eb; + border-radius: 2px; background: #fff; } @@ -205,16 +270,16 @@ .employee-risk-evidence-row { min-width: 0; display: grid; - gap: 5px; - padding: 8px; + gap: 0; border: 1px solid #e2e8f0; - border-radius: 4px; + border-radius: 2px; background: #f8fafc; + overflow: hidden; } .employee-risk-evidence-row.medium { - border-color: #fed7aa; - background: #fffbf5; + border-color: #f3e8d9; + background: #fffcf7; } .employee-risk-evidence-row.high { @@ -222,12 +287,26 @@ background: #fff7f7; } +.employee-risk-evidence-row[open] { + background: #fff; +} + +.employee-risk-evidence-row summary { + list-style: none; + cursor: pointer; +} + +.employee-risk-evidence-row summary::-webkit-details-marker { + display: none; +} + .employee-risk-evidence-title { - min-height: 20px; + min-height: 40px; display: flex; align-items: center; justify-content: space-between; gap: 8px; + padding: 8px 10px; color: #0f172a; font-size: 11px; font-weight: 850; @@ -262,13 +341,26 @@ color: #b91c1c; } +.employee-risk-evidence-title::after { + content: '展开'; + flex: 0 0 auto; + color: #94a3b8; + font-size: 10px; + font-weight: 800; +} + +.employee-risk-evidence-row[open] .employee-risk-evidence-title::after { + content: '收起'; +} + .employee-risk-evidence-row ul { display: grid; gap: 3px; margin: 0; - padding: 0; + padding: 0 10px 10px 10px; list-style: none; align-content: start; + border-top: 1px solid #e2e8f0; } .employee-risk-evidence-row li { @@ -291,6 +383,10 @@ grid-template-columns: 1fr; } + .employee-risk-review-item { + flex-basis: 100%; + } + .employee-risk-title-wrap, .employee-risk-section-head { flex-wrap: wrap; diff --git a/web/src/assets/styles/components/top-bar.css b/web/src/assets/styles/components/top-bar.css index 394a86a..a53f63e 100644 --- a/web/src/assets/styles/components/top-bar.css +++ b/web/src/assets/styles/components/top-bar.css @@ -380,6 +380,14 @@ min-width: 0; } +.topbar-utility-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + min-width: 0; +} + .topbar-icon-btn { position: relative; width: 34px; @@ -1113,6 +1121,68 @@ font-size: 16px; } +.topbar-ai-mode-toggle { + flex: 0 0 38px; + width: 38px; + height: 38px; + display: inline-grid; + place-items: center; + padding: 0; + border: 2px solid transparent; + border-radius: 50%; + background: + linear-gradient(#ffffff, #ffffff) padding-box, + conic-gradient(from 210deg, #15b8c8, #4f6fef, #b65cff, #ec4899, #f59e0b, #15b8c8) border-box; + box-shadow: + 0 8px 18px rgba(79, 111, 239, 0.16), + 0 0 0 1px rgba(255, 255, 255, 0.78) inset; + transition: + transform 180ms var(--ease), + box-shadow 180ms var(--ease), + filter 180ms var(--ease); +} + +.topbar-ai-mode-toggle__glyph { + display: inline-block; + background: linear-gradient(135deg, #0ea5b7 4%, #4f6fef 34%, #a855f7 58%, #ec4899 76%, #f59e0b 96%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + font-size: 15px; + font-weight: 950; + line-height: 1; + letter-spacing: 0; +} + +.topbar-ai-mode-toggle:hover, +.topbar-ai-mode-toggle:focus-visible { + transform: translateY(-1px); + box-shadow: + 0 12px 24px rgba(79, 111, 239, 0.2), + 0 0 0 4px rgba(236, 72, 153, 0.08), + 0 0 0 1px rgba(255, 255, 255, 0.86) inset; +} + +.topbar-ai-mode-toggle:focus-visible { + outline: 2px solid color-mix(in srgb, var(--theme-primary-active) 72%, #ffffff); + outline-offset: 3px; +} + +.topbar-ai-mode-toggle.active { + filter: saturate(1.1); + box-shadow: + 0 12px 24px rgba(79, 111, 239, 0.22), + 0 0 0 4px rgba(14, 165, 183, 0.09), + 0 0 0 1px rgba(255, 255, 255, 0.88) inset; +} + +.topbar-ai-mode-toggle:not(.active) { + filter: saturate(0.82); + box-shadow: + 0 6px 14px rgba(15, 23, 42, 0.1), + 0 0 0 1px rgba(255, 255, 255, 0.82) inset; +} + .kpi-chip { display: grid; grid-template-columns: auto auto; @@ -1259,6 +1329,10 @@ gap: 12px; } + .topbar-utility-actions { + gap: 8px; + } + .topbar-icon-btn { width: 30px; height: 30px; @@ -1271,6 +1345,16 @@ font-size: 12px; } + .topbar-ai-mode-toggle { + flex: 0 0 34px; + width: 34px; + height: 34px; + } + + .topbar-ai-mode-toggle__glyph { + font-size: 14px; + } + .kpi-chips { gap: 8px; } @@ -1329,6 +1413,7 @@ .search-wrap, .search-wrap.wide, .topbar-toolset, + .topbar-utility-actions, .detail-alert-strip, .month-chip, .qa-filter, @@ -1344,6 +1429,15 @@ justify-content: flex-end; } + .topbar-utility-actions { + justify-content: flex-end; + } + + .topbar-ai-mode-toggle { + width: 34px; + height: 34px; + } + .range-shell { flex: 1; } @@ -1505,6 +1599,10 @@ justify-content: space-between; } + .topbar-ai-mode-toggle { + flex: 0 0 34px; + } + .range-combo { display: grid; gap: 8px; diff --git a/web/src/assets/styles/components/travel-reimbursement-message-item.css b/web/src/assets/styles/components/travel-reimbursement-message-item.css index b223fde..59415f0 100644 --- a/web/src/assets/styles/components/travel-reimbursement-message-item.css +++ b/web/src/assets/styles/components/travel-reimbursement-message-item.css @@ -56,16 +56,52 @@ object-fit: cover; } -.message-bubble { - max-width: min(100%, 760px); - padding: 12px 14px; - border: 1px solid #d8e4f0; - border-radius: 4px; - background: #ffffff; - color: #24324a; - font-size: var(--wb-fs-bubble, 13px); - line-height: 1.62; - box-shadow: 0 10px 22px rgba(148, 163, 184, 0.14); +.message-bubble-compact-guidance { + max-width: min(100%, 640px); + padding: 10px 12px; + border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.18); + background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%); + box-shadow: 0 8px 18px rgba(148, 163, 184, 0.12); +} + +.message-bubble-compact-guidance .message-meta { + margin-bottom: 6px; +} + +.message-bubble-compact-guidance .message-meta strong { + font-size: 12px; +} + +.message-bubble-compact-guidance .message-answer-content { + font-size: 12px; +} + +.message-bubble-compact-guidance .message-answer-markdown { + display: grid; + gap: 6px; +} + +.message-bubble-compact-guidance .message-answer-markdown :deep(h3) { + margin: 0; + padding-left: 8px; + border-left: 3px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.42); + color: #17324a; + font-size: 12px; + font-weight: 860; + line-height: 1.4; +} + +.message-bubble-compact-guidance .message-answer-markdown :deep(ul), +.message-bubble-compact-guidance .message-answer-markdown :deep(ol) { + gap: 4px; +} + +.message-bubble-compact-guidance .message-answer-markdown :deep(li) { + line-height: 1.55; +} + +.message-bubble-compact-guidance .message-suggested-actions { + margin-top: 8px; } .message-row.has-steward-plan .message-bubble { @@ -135,7 +171,7 @@ .steward-intent-event-list { margin: 0; - padding: 0 12px 12px 30px; + padding: 0 12px 12px 44px; display: grid; gap: 7px; } @@ -274,6 +310,42 @@ color: #24324a; } +.message-answer-markdown { + display: grid; + gap: 8px; + word-break: break-word; + overflow-wrap: anywhere; +} + +.message-answer-markdown :deep(h1), +.message-answer-markdown :deep(h2), +.message-answer-markdown :deep(h3), +.message-answer-markdown :deep(h4) { + margin: 0; + color: #0f172a; + line-height: 1.35; +} + +.message-answer-markdown :deep(h1) { + font-size: 15px; + font-weight: 860; +} + +.message-answer-markdown :deep(h2) { + font-size: 14px; + font-weight: 850; +} + +.message-answer-markdown :deep(h3) { + font-size: 13px; + font-weight: 840; +} + +.message-answer-markdown :deep(h4) { + font-size: 12px; + font-weight: 820; +} + .message-answer-markdown :deep(p), .message-answer-markdown :deep(li), .message-answer-markdown :deep(td), @@ -281,16 +353,66 @@ .message-answer-markdown :deep(blockquote) { margin: 0; color: inherit; - line-height: 1.62; + line-height: 1.6; } .message-answer-markdown :deep(p + p), .message-answer-markdown :deep(p + ul), +.message-answer-markdown :deep(p + ol), .message-answer-markdown :deep(ul + p), -.message-answer-markdown :deep(ol + p) { +.message-answer-markdown :deep(ol + p), +.message-answer-markdown :deep(blockquote + p) { margin-top: 8px; } +.message-answer-markdown :deep(ul), +.message-answer-markdown :deep(ol) { + margin: 0; + padding-left: 1.2em; + display: grid; + gap: 6px; +} + +.message-answer-markdown :deep(li) { + margin: 0; +} + +.message-answer-markdown :deep(li > p) { + margin: 0; +} + +.message-answer-markdown :deep(blockquote) { + padding: 8px 10px; + border-left: 3px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.38); + border-radius: 4px; + background: #f8fbff; + color: #475569; +} + +.message-answer-markdown :deep(code) { + padding: 0 5px; + border-radius: 4px; + background: #eef6fb; + color: #1d4ed8; + font-size: 0.95em; +} + +.message-answer-markdown :deep(pre) { + margin: 0; + padding: 10px 12px; + overflow: auto; + border-radius: 4px; + background: #0f172a; + color: #e2e8f0; +} + +.message-answer-markdown :deep(pre code) { + padding: 0; + background: transparent; + color: inherit; + font-size: 12px; +} + .message-answer-markdown :deep(strong) { color: #0f172a; font-weight: 850; @@ -649,6 +771,40 @@ gap: 8px; } +.message-bubble-compact-guidance .message-suggested-actions.compact-guidance-actions { + grid-template-columns: repeat(auto-fit, minmax(136px, 1fr)); + gap: 6px; +} + +.message-bubble-compact-guidance .message-suggested-actions.compact-guidance-actions .message-suggested-action-btn { + min-height: 40px; + padding: 8px 10px; + grid-template-columns: 22px minmax(0, 1fr); + gap: 8px; + border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.18); + background: #ffffff; +} + +.message-bubble-compact-guidance .message-suggested-actions.compact-guidance-actions .message-suggested-action-icon { + width: 22px; + height: 22px; + font-size: 13px; +} + +.message-bubble-compact-guidance .message-suggested-actions.compact-guidance-actions .message-suggested-action-copy { + gap: 0; +} + +.message-bubble-compact-guidance .message-suggested-actions.compact-guidance-actions .message-suggested-action-title { + font-size: 12px; + line-height: 1.35; +} + +.message-bubble-compact-guidance .message-suggested-actions.compact-guidance-actions .message-suggested-action-btn small, +.message-bubble-compact-guidance .message-suggested-actions.compact-guidance-actions .message-suggested-action-arrow { + display: none; +} + .structured-card-reveal-enter-active { transition: opacity 220ms cubic-bezier(0.2, 0, 0, 1), diff --git a/web/src/assets/styles/views/personal-workbench-view.css b/web/src/assets/styles/views/personal-workbench-view.css new file mode 100644 index 0000000..4455046 --- /dev/null +++ b/web/src/assets/styles/views/personal-workbench-view.css @@ -0,0 +1,40 @@ +.workbench-mode-fade-enter-active, +.workbench-mode-fade-leave-active { + transition: + opacity 220ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)), + transform 220ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)), + filter 220ms var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1)); + transform-origin: 50% 24px; + will-change: opacity, transform, filter; +} + +.workbench-mode-fade-enter-from, +.workbench-mode-fade-leave-to { + opacity: 0; + transform: translateY(10px) scale(0.992); + filter: blur(2px); +} + +.workbench-mode-fade-enter-to, +.workbench-mode-fade-leave-from { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0); +} + +@media (prefers-reduced-motion: reduce) { + .workbench-mode-fade-enter-active, + .workbench-mode-fade-leave-active { + transition: none; + will-change: auto; + } + + .workbench-mode-fade-enter-from, + .workbench-mode-fade-leave-to, + .workbench-mode-fade-enter-to, + .workbench-mode-fade-leave-from { + opacity: 1; + transform: none; + filter: none; + } +} diff --git a/web/src/assets/workbench-ai-mode-orb-icon.gif b/web/src/assets/workbench-ai-mode-orb-icon.gif new file mode 100644 index 0000000..24a7124 Binary files /dev/null and b/web/src/assets/workbench-ai-mode-orb-icon.gif differ diff --git a/web/src/assets/workbench-ai-mode-orb-icon.png b/web/src/assets/workbench-ai-mode-orb-icon.png new file mode 100644 index 0000000..10bffbf Binary files /dev/null and b/web/src/assets/workbench-ai-mode-orb-icon.png differ diff --git a/web/src/assets/workbench-ai-mode-robot-bg.png b/web/src/assets/workbench-ai-mode-robot-bg.png new file mode 100644 index 0000000..77459f5 Binary files /dev/null and b/web/src/assets/workbench-ai-mode-robot-bg.png differ diff --git a/web/src/components/business/PersonalWorkbench.vue b/web/src/components/business/PersonalWorkbench.vue index 597c20b..d036565 100644 --- a/web/src/components/business/PersonalWorkbench.vue +++ b/web/src/components/business/PersonalWorkbench.vue @@ -7,168 +7,35 @@ note="把费用申请、报销进度、制度问答和待办处理集中到一个入口。" /> -
-
-

- {{ typedTitlePrefix }}小财管家| -

+
+
+
+

报销趋势

+

{{ reimbursementTrendRangeLabel }}

+ {{ reimbursementTrendTotalLabel }} + + + {{ reimbursementTrendGrowthLabel }} 同比去年同期 + + {{ displayUserName }} · {{ reimbursementTrendSignalLabel }} +
- +
+
+ 月度报销明细 + 与分析看板同源 +
-
- +
+ +
+
+
+ + + +
+ + +
+ +
+
+ {{ displayModelName }} + +
+ +
+
+ + +
+ 已选择 {{ selectedFiles.length }} 份附件 + +
+ +
+

快速开始

+
+ +
+
+
+ +
+
+ + +
+ +
+
+ {{ activeConversationTitle || '新对话' }} +

直接输入问题,小财管家会在当前页面内持续回复。

+
+ +
+
+ {{ message.content }} +
+
+ + + +
+ + +
+
+ +
+
+ 已选择 {{ selectedFiles.length }} 份附件 + +
+ +
+
+
+ + {{ workbenchDateTagLabel }} + +
+ + +
+ +
+
+
+ + + +
+ + +
+ +
+
+ {{ displayModelName }} + +
+ +
+
+
+ +

小财管家可能会出错,重要费用事项请核对单据、制度与审批结果。

+
+
+ + + + + + + + + + + diff --git a/web/src/components/charts/TrendChart.vue b/web/src/components/charts/TrendChart.vue index 0dbb9d0..6ed89ac 100644 --- a/web/src/components/charts/TrendChart.vue +++ b/web/src/components/charts/TrendChart.vue @@ -1,5 +1,5 @@ diff --git a/web/src/components/layout/TopBar.vue b/web/src/components/layout/TopBar.vue index 60927d6..51d4252 100644 --- a/web/src/components/layout/TopBar.vue +++ b/web/src/components/layout/TopBar.vue @@ -1,10 +1,11 @@  - -
- - + + +
+ + +
+
+ + diff --git a/web/src/views/PersonalWorkbenchView.vue b/web/src/views/PersonalWorkbenchView.vue index 6b7364a..90b154d 100644 --- a/web/src/views/PersonalWorkbenchView.vue +++ b/web/src/views/PersonalWorkbenchView.vue @@ -1,20 +1,36 @@ + + diff --git a/web/src/views/scripts/TravelReimbursementCreateView.js b/web/src/views/scripts/TravelReimbursementCreateView.js index 452e14b..5c8641c 100644 --- a/web/src/views/scripts/TravelReimbursementCreateView.js +++ b/web/src/views/scripts/TravelReimbursementCreateView.js @@ -24,7 +24,8 @@ import { import { useApplicationPreviewEditor } from './useApplicationPreviewEditor.js' import { buildStewardFieldCompletionContinuation, - buildStewardFieldCompletionRawText + buildStewardFieldCompletionRawText, + resolveStewardRuntimeFieldCompletion } from './stewardFieldCompletionModel.js' import { buildOperationFeedbackPayload, @@ -169,8 +170,6 @@ import { buildFileIdentity, buildFilePreviews, buildOcrDocumentsFromReviewPayload, - buildOcrFilePreviews, - buildOcrSummary, buildOcrSummaryFromDocuments, buildReviewFilePreviewsFromReviewPayload, extractReviewAttachmentNames, @@ -179,7 +178,6 @@ import { mergeFilesWithLimit, mergeUploadAttachmentNames, mergeUploadOcrDocuments, - normalizeOcrDocuments, resolveAttachmentPreviewKind, resolveDocumentPreview } from './travelReimbursementAttachmentModel.js' @@ -1121,8 +1119,6 @@ export default { buildExpenseSceneSelectionMessage, buildMessageMeta, buildOcrDocumentsFromReviewPayload, - buildOcrFilePreviews, - buildOcrSummary, buildOcrSummaryFromDocuments, buildReviewFormContextFromPayload, clearAttachedFiles, @@ -1155,7 +1151,6 @@ export default { messages, nextTick, normalizeExpenseQueryPayload, - normalizeOcrDocuments, persistSessionState, props, recognizeOcrFiles, @@ -1904,6 +1899,10 @@ export default { }) return } + if (targetSessionType === SESSION_TYPE_EXPENSE && carryText === '我要报销') { + pushExpenseSceneSelectionPrompt(carryText) + return + } if (String(actionPayload.steward_plan_id || '').trim()) { const confirmedByText = Boolean(action.confirmedByText) delete action.confirmedByText @@ -2141,6 +2140,9 @@ export default { } function buildMessageBubbleClass(message) { + if (message?.role === 'assistant' && message?.assistantVariant === 'compact_guidance') { + return 'message-bubble-compact-guidance' + } if (message?.role === 'assistant' && message?.budgetReport) { return 'message-bubble-budget-report' } @@ -2965,6 +2967,10 @@ export default { : '当前申请还有信息需要先补充。请先回复系统刚刚追问的内容,我再继续生成核对结果。' } } + const fieldCompletionDecision = resolveStewardRuntimeFieldCompletion(normalizedText, runtimeState) + if (fieldCompletionDecision) { + return fieldCompletionDecision + } } return null } @@ -3082,6 +3088,39 @@ export default { }) return true } + if (nextAction === 'fill_current_application_field') { + const targetMessageId = String(decision?.target_message_id || decision?.targetMessageId || '').trim() + const targetMessage = targetMessageId + ? messages.value.find((message) => String(message.id || '') === targetMessageId) + : findLatestApplicationPreviewMessage() + if (!targetMessage?.applicationPreview) { + return false + } + const fieldKey = String(decision?.field_key || decision?.fieldKey || '').trim() + const fieldLabel = String(decision?.field_label || decision?.fieldLabel || '').trim() + const fieldValue = String(decision?.field_value || decision?.fieldValue || rawText).trim() + if (!fieldKey || !fieldValue) { + return false + } + await continueStewardApplicationFieldCompletion({ + targetMessage, + action: { + label: fieldValue, + suppressUserEcho: userMessageAlreadyAdded, + payload: { + steward_delegated_field_completion: true, + field_key: fieldKey, + field_label: fieldLabel, + value: fieldValue + } + }, + sourcePreview: targetMessage.applicationPreview, + fieldKey, + fieldLabel, + value: fieldValue + }) + return true + } if (nextAction === 'ask_user' || nextAction === 'cancel_current_action' || nextAction === 'no_op') { pushStewardRuntimeResponse(rawText, decision, { userMessageAlreadyAdded }) return true diff --git a/web/src/views/scripts/TravelRequestDetailView.js b/web/src/views/scripts/TravelRequestDetailView.js index 1d9daae..567b44b 100644 --- a/web/src/views/scripts/TravelRequestDetailView.js +++ b/web/src/views/scripts/TravelRequestDetailView.js @@ -1751,12 +1751,12 @@ export default { const aiAdviceTitle = computed(() => { if (!isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value) { - return '报销风险提示' + return '风险提示' } if (isEditableRequest.value && isApplicationDocument.value) { return '表单自查提示' } - return isEditableRequest.value ? 'AI建议' : 'AI提示' + return isEditableRequest.value ? 'AI建议' : '风险提示' }) const aiAdviceHint = computed(() => ( !isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value diff --git a/web/src/views/scripts/stewardFieldCompletionModel.js b/web/src/views/scripts/stewardFieldCompletionModel.js index c65aef2..0bd4ab5 100644 --- a/web/src/views/scripts/stewardFieldCompletionModel.js +++ b/web/src/views/scripts/stewardFieldCompletionModel.js @@ -24,6 +24,35 @@ const APPLICATION_PREVIEW_FIELD_LABEL_MAP = { grade: '职级' } +const STEWARD_RUNTIME_FIELD_COMPLETION_RULES = [ + { fieldKey: 'reason', fieldLabel: '事由', pattern: /事由|申请事由|出差事由|原因|用途/ }, + { fieldKey: 'transportMode', fieldLabel: '出行方式', pattern: /出行方式|交通方式|交通工具|出行工具/ }, + { fieldKey: 'time', fieldLabel: '申请时间', pattern: /申请时间|发生时间|业务发生时间|出发时间|返回时间|时间/ }, + { fieldKey: 'location', fieldLabel: '地点', pattern: /地点|业务地点|发生地点|目的地/ }, + { fieldKey: 'days', fieldLabel: '天数', pattern: /天数|出差天数|申请天数/ }, + { fieldKey: 'amount', fieldLabel: '系统预估费用', pattern: /系统预估费用|用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额/ } +] + +const APPLICATION_TYPE_DISPLAY_MAP = { + travel: '差旅费用申请', + travel_application: '差旅费用申请', + expense_application: '费用申请', + application: '费用申请', + transportation: '交通费用申请', + traffic: '交通费用申请', + transport: '交通费用申请', + accommodation: '住宿费用申请', + hotel: '住宿费用申请', + meeting: '会务费用申请', + conference: '会务费用申请', + purchase: '采购费用申请', + procurement: '采购费用申请', + training: '培训费用申请', + business_entertainment: '业务招待申请', + entertainment: '业务招待申请', + office: '办公费用申请' +} + function compactValue(value = '') { return String(value || '').trim() } @@ -48,6 +77,22 @@ function resolveFieldValue(...candidates) { return '' } +function resolveApplicationTypeDisplay(value = '') { + const rawValue = compactValue(value) + if (!rawValue) return '' + + const normalizedKey = rawValue.toLowerCase() + if (APPLICATION_TYPE_DISPLAY_MAP[normalizedKey]) { + return APPLICATION_TYPE_DISPLAY_MAP[normalizedKey] + } + if (/^(?:差旅费|差旅|出差)$/.test(rawValue)) return '差旅费用申请' + if (/^(?:交通费|交通)$/.test(rawValue)) return '交通费用申请' + if (/^(?:住宿费|住宿|酒店)$/.test(rawValue)) return '住宿费用申请' + if (/^(?:会务|会议|会务费)$/.test(rawValue)) return '会务费用申请' + if (/^(?:采购|采购费|办公用品)$/.test(rawValue)) return '采购费用申请' + return rawValue +} + function buildUpdatedTask(task = null, fieldKey = '', value = '') { if (!task || typeof task !== 'object') { return null @@ -75,6 +120,29 @@ function buildUpdatedTask(task = null, fieldKey = '', value = '') { } } +function buildFieldCompletionScopeHints(fieldKey = '', selectedValue = '') { + const hints = [ + '本轮是对当前申请单字段的补充/更新,不是新建申请或切换任务。' + ] + if (fieldKey === 'reason') { + hints.push( + `请将“${compactValue(selectedValue)}”作为当前出差申请的事由继续处理,不要把它改判为新的 IT 部署申请。` + ) + } + return hints +} + +function resolveFieldRuleByKey(fieldKey = '') { + const normalizedKey = compactValue(fieldKey) + return STEWARD_RUNTIME_FIELD_COMPLETION_RULES.find((rule) => rule.fieldKey === normalizedKey) || null +} + +function resolveFieldRuleByLabel(label = '') { + const normalizedLabel = compactValue(label) + if (!normalizedLabel) return null + return STEWARD_RUNTIME_FIELD_COMPLETION_RULES.find((rule) => rule.pattern.test(normalizedLabel)) || null +} + export function buildStewardFieldCompletionContinuation(continuation = null, fieldKey = '', value = '') { const source = continuation && typeof continuation === 'object' ? continuation : {} const currentTask = resolveStewardCurrentTask(source) @@ -89,6 +157,50 @@ export function buildStewardFieldCompletionContinuation(continuation = null, fie } } +export function resolveStewardRuntimeFieldCompletion(rawText = '', runtimeState = {}) { + const value = compactValue(rawText) + if (!value || compactValue(runtimeState?.waiting_for) !== 'application_field_completion') { + return null + } + + const slotAction = runtimeState?.pending_slot_action || runtimeState?.pendingSlotAction || null + const slotPayload = slotAction?.payload && typeof slotAction.payload === 'object' ? slotAction.payload : {} + const slotFieldKey = compactValue(slotPayload.field_key || slotPayload.fieldKey || slotAction?.field_key || slotAction?.fieldKey) + const slotRule = resolveFieldRuleByKey(slotFieldKey) + if (slotRule) { + return { + next_action: 'fill_current_application_field', + target_message_id: compactValue(slotAction?.message_id || slotAction?.messageId), + field_key: slotRule.fieldKey, + field_label: slotRule.fieldLabel, + field_value: value + } + } + + const pendingApplication = runtimeState?.pending_application || runtimeState?.pendingApplication || null + const missingFields = Array.isArray(pendingApplication?.missing_fields) + ? pendingApplication.missing_fields + : Array.isArray(pendingApplication?.missingFields) + ? pendingApplication.missingFields + : [] + if (missingFields.length !== 1) { + return null + } + + const rule = resolveFieldRuleByLabel(missingFields[0]) + if (!rule) { + return null + } + + return { + next_action: 'fill_current_application_field', + target_message_id: compactValue(pendingApplication?.message_id || pendingApplication?.messageId), + field_key: rule.fieldKey, + field_label: rule.fieldLabel, + field_value: value + } +} + export function buildStewardFieldCompletionRawText({ preview = {}, fieldKey = '', @@ -107,7 +219,12 @@ export function buildStewardFieldCompletionRawText({ : resolveFieldValue(fields.transportMode, ontologyFields.transport_mode) const knownLines = [ - ['申请类型', resolveFieldValue(fields.applicationType, ontologyFields.expense_type, '差旅费用申请')], + [ + '申请类型', + resolveApplicationTypeDisplay( + resolveFieldValue(fields.applicationType, ontologyFields.expense_type, '差旅费用申请') + ) + ], ['时间', resolveFieldValue(fields.time, ontologyFields.time_range)], ['地点', resolveFieldValue(fields.location, ontologyFields.location)], ['事由', resolveFieldValue(fields.reason, ontologyFields.reason, currentTask?.summary)], @@ -120,6 +237,7 @@ export function buildStewardFieldCompletionRawText({ return [ '小财管家继续执行申请单字段补齐。', `用户已补充:${selectedLabel}:${selectedValue}。`, + ...buildFieldCompletionScopeHints(fieldKey, selectedValue), currentTask?.summary ? `任务摘要:${currentTask.summary}` : '', '', '已识别信息:', diff --git a/web/src/views/scripts/stewardPlanModel.js b/web/src/views/scripts/stewardPlanModel.js index e0ab88f..d6fc937 100644 --- a/web/src/views/scripts/stewardPlanModel.js +++ b/web/src/views/scripts/stewardPlanModel.js @@ -99,6 +99,10 @@ const FIELD_VALUE_DISPLAY_CONFIG = { } } +const FLOW_EXPENSE_TYPE_LABELS = { + travel: '差旅费' +} + export function buildStewardPlanRequest({ rawText = '', files = [], @@ -216,6 +220,10 @@ export function buildStewardPlanMessageText(plan) { if (isPendingFlowConfirmationPlan(normalized)) { return buildPendingFlowConfirmationMessageText(normalized) } + const genericReimbursementTask = normalized.tasks.find((task) => isGenericReimbursementTask(task)) + if (genericReimbursementTask && normalized.tasks.length === 1) { + return buildGenericReimbursementIntentMessageText(genericReimbursementTask) + } const nextContext = resolveNextActionContext(normalized) const orderedTasks = buildOrderedStewardTasks(normalized, nextContext?.task) const taskLines = orderedTasks.map((task, index) => @@ -289,6 +297,42 @@ export function formatStewardOntologyFields(fields = {}, taskType = '') { .join(';') } +function buildStewardOntologyFieldRows(fields = {}, taskType = '') { + return Object.entries(fields || {}) + .filter(([, value]) => String(value || '').trim()) + .map(([key, value]) => { + const field = resolveFieldDisplay(key, taskType) + return { + label: field.label, + value: formatStewardFieldDisplayValue(field.key, value) + } + }) +} + +function escapeMarkdownTableCell(value) { + return String(value || '').replace(/\|/g, '\\|').replace(/\n+/g, ' ').trim() +} + +function formatStewardOntologyFieldsTable(fields = {}, taskType = '') { + const rows = buildStewardOntologyFieldRows(fields, taskType) + if (!rows.length) { + return '' + } + return [ + '| 字段 | 内容 |', + '| --- | --- |', + ...rows.map((row) => `| ${escapeMarkdownTableCell(row.label)} | ${escapeMarkdownTableCell(row.value)} |`) + ].join('\n') +} + +function resolveCandidateFlowExpenseType(flow = {}) { + const rawType = String(flow?.ontologyFields?.expense_type || flow?.ontologyFields?.expenseType || '').trim() + if (rawType === '差旅' || rawType === 'travel') { + return 'travel' + } + return rawType +} + export function buildStewardSuggestedActions(plan) { const normalized = normalizeStewardPlan(plan) if (isOffTopicPlan(normalized)) { @@ -304,26 +348,32 @@ export function buildStewardSuggestedActions(plan) { })) } if (isPendingFlowConfirmationPlan(normalized)) { - return normalized.candidateFlows.map((flow) => ({ - label: flow.label, - description: flow.reason || '选择后小财管家会继续整理对应流程材料。', - icon: flow.flowId === 'travel_application' - ? 'mdi mdi-file-plus-outline' - : 'mdi mdi-receipt-text-plus-outline', - action_type: ASSISTANT_SCOPE_ACTION_SWITCH, - payload: { - steward_confirm_flow: true, - steward_plan_id: normalized.planId, - flow_id: flow.flowId, - session_type: flow.flowId === 'travel_application' - ? SESSION_TYPE_APPLICATION - : SESSION_TYPE_EXPENSE, - selected_flow_label: flow.label, - carry_text: flow.label, - auto_submit: true, - steward_state: normalized.stewardState || null + return normalized.candidateFlows.map((flow) => { + const expenseType = resolveCandidateFlowExpenseType(flow) + return { + label: flow.label, + description: flow.reason || '选择后小财管家会继续整理对应流程材料。', + icon: flow.flowId === 'travel_application' + ? 'mdi mdi-file-plus-outline' + : 'mdi mdi-receipt-text-plus-outline', + action_type: ASSISTANT_SCOPE_ACTION_SWITCH, + payload: { + steward_confirm_flow: true, + steward_plan_id: normalized.planId, + flow_id: flow.flowId, + session_type: flow.flowId === 'travel_application' + ? SESSION_TYPE_APPLICATION + : SESSION_TYPE_EXPENSE, + selected_flow_label: flow.label, + expense_type: expenseType, + expense_type_label: FLOW_EXPENSE_TYPE_LABELS[expenseType] || '', + requires_application_before_reimbursement: flow.flowId === 'travel_reimbursement' && expenseType === 'travel', + carry_text: flow.flowId === 'travel_reimbursement' && expenseType === 'travel' ? '我要报销' : flow.label, + auto_submit: true, + steward_state: normalized.stewardState || null + } } - })) + }) } const nextContext = resolveNextActionContext(normalized) if (!nextContext) { @@ -335,7 +385,7 @@ export function buildStewardSuggestedActions(plan) { : SESSION_TYPE_EXPENSE return [ { - label: buildNextActionLabel(actionType), + label: buildNextActionLabel(actionType, task), description: buildNextActionDescription(actionType, normalized, task, group), icon: actionType === 'confirm_create_application' ? 'mdi mdi-file-plus-outline' @@ -411,40 +461,58 @@ export function isOffTopicStewardPlan(rawPlan) { } function buildOffTopicMessageText(normalized) { + // off_topic 计划的引导文案完全由后端生成(含 ### 标题 + 正文 + 引导句), + // 前端透传 summary 即可,避免重复拼接导致与后端表达不一致。 const summary = String(normalized?.summary || '').trim() - const summaryLine = summary && summary !== '这看起来跟财务任务没什么关系...' - ? summary - : '这看起来跟财务任务没什么关系,我目前只能帮你处理**费用申请**和**费用报销**两类事项。' - return [ - '### 小财管家没看懂这件事', - '', - summaryLine, - '', - '你可以试试下面这些方式告诉我:' - ].join('\n') + if (summary) { + return summary + } + return ( + '### 这句话我暂时没识别到财务事项\n\n' + + '很抱歉主人,目前小财管家只能帮您整理**费用申请**和**费用报销**这两类事项。\n\n' + + '要不您换种说法告诉我:' + ) } function buildPendingFlowConfirmationMessageText(normalized) { const fields = normalized.candidateFlows[0]?.ontologyFields || {} - const knownParts = formatStewardOntologyFields(fields, 'expense_application') + const knownTable = formatStewardOntologyFieldsTable(fields, 'expense_application') const candidateLines = normalized.candidateFlows.map((flow, index) => `${index + 1}. **${flow.label}**${flow.reason ? `\n - ${flow.reason}` : ''}` ) + const singleCandidate = normalized.candidateFlows.length === 1 return [ '### 需要先确认流程方向', '', - knownParts - ? `我识别到这是一项财务事项,已提取到:**${knownParts}**。` + knownTable + ? ['我识别到这是一项财务事项,已提取到:', '', knownTable].join('\n') : '我识别到这是一项财务事项,但还需要确认你要进入哪个流程。', '', normalized.pendingFlowConfirmation.reason || normalized.summary || '当前还不能确定你要补办申请还是发起报销。', '', ...candidateLines, '', - '请先选择一个方向,我会继续整理对应材料。' + singleCandidate + ? `请先点击下方 **${normalized.candidateFlows[0].label}**,我会继续整理对应材料。` + : '请先选择一个方向,我会继续整理对应材料。' ].filter((line, index, lines) => line || lines[index - 1]).join('\n') } +function buildGenericReimbursementIntentMessageText() { + return [ + '### 我来带你发起报销', + '', + '你现在只说了要报销,还没告诉我具体是哪类费用。先不用一次性补全所有信息,我会按报销流程一步步带你填。', + '', + '1. **先选报销场景**', + ' - 例如差旅费、交通费、住宿费、业务招待费或办公用品费,不同场景需要的材料不一样。', + '2. **再补关键材料**', + ' - 我会继续追问事由、发生时间、金额和票据附件;如果是差旅或招待,还会先帮你核对是否需要关联事前申请。', + '', + '点击下面的 **确定,选择报销场景**,我会进入报销助手继续引导。' + ].join('\n') +} + function resolveNextActionContext(normalized) { const applicationTask = normalized.tasks.find((task) => task.taskType === 'expense_application') const applicationAction = applicationTask @@ -566,6 +634,9 @@ function buildTaskOrderActionDescription(task) { return `我会请${agent}先把申请单草稿整理出来,方便你核对关键信息,再决定是否继续。` } if (task.taskType === 'reimbursement') { + if (isGenericReimbursementTask(task)) { + return `我会请${agent}先带你选择报销场景,再逐步补齐事由、时间、金额和票据。` + } return `我会请${agent}把票据、金额和制度口径先核清楚,前一步确认后再继续往下走。` } return `我会请${agent}先整理可核对的结果,真正执行前仍会让你确认。` @@ -603,13 +674,16 @@ function buildNextTaskLead(task) { return `处理“${task.title || task.taskTypeLabel}”` } -function buildNextActionLabel(actionType) { +function buildNextActionLabel(actionType, task = null) { if (actionType === 'confirm_create_application') { return '确定,先创建申请单' } if (actionType === 'confirm_attachment_group') { return '确定,确认附件归集' } + if (isGenericReimbursementTask(task)) { + return '确定,选择报销场景' + } return '确定,继续填写报销单' } @@ -627,7 +701,29 @@ function buildNextActionDescription(actionType, normalized, task, group) { } return group?.attachmentNames?.length ? `报销助手会带入 ${group.attachmentNames.length} 份相关附件生成核对结果。` - : '报销助手会根据当前任务生成报销核对结果。' + : isGenericReimbursementTask(task) + ? '先进入报销助手选择具体费用类型,再按场景补齐事由、时间、金额和票据。' + : '报销助手会根据当前任务生成报销核对结果。' +} + +function isGenericReimbursementTask(task) { + if (!task || task.taskType !== 'reimbursement') { + return false + } + const fields = task.ontologyFields || {} + const expenseType = String(fields.expense_type || '').trim() + const hasSpecificField = ['time_range', 'location', 'amount', 'attachments', 'transport_mode'] + .some((key) => String(fields[key] || '').trim()) + || isSpecificReimbursementReason(fields.reason) + return !hasSpecificField && (!expenseType || expenseType === 'other') +} + +function isSpecificReimbursementReason(value) { + const text = String(value || '').trim().replace(/\s+/g, '') + if (!text) { + return false + } + return !/^(?:我想要|我想|我要|还需要|需要|请帮我|帮我)?报销(?:费用|报销单|报销流程)?$/.test(text) } function buildStewardCarryText(actionType, task, group, normalized = null) { @@ -644,6 +740,9 @@ function buildStewardCarryText(actionType, task, group, normalized = null) { if (!task) { return '我确认继续处理这项财务任务,请按现有流程核对信息。' } + if (actionType === 'confirm_create_reimbursement_draft' && isGenericReimbursementTask(task)) { + return '我要报销' + } const fields = formatStewardOntologyFields(task.ontologyFields || {}, task.taskType) const missingFields = formatStewardMissingFieldList( diff --git a/web/src/views/scripts/travelReimbursementAttachmentModel.js b/web/src/views/scripts/travelReimbursementAttachmentModel.js index 8339860..60d0ad5 100644 --- a/web/src/views/scripts/travelReimbursementAttachmentModel.js +++ b/web/src/views/scripts/travelReimbursementAttachmentModel.js @@ -74,6 +74,10 @@ export function normalizeOcrDocuments(payload) { preview_kind: String(item.preview_kind || '').trim(), preview_data_url: String(item.preview_data_url || '').trim(), preview_url: String(item.preview_url || '').trim(), + receipt_id: String(item.receipt_id || item.receiptId || '').trim(), + receipt_status: String(item.receipt_status || item.receiptStatus || '').trim(), + receipt_preview_url: String(item.receipt_preview_url || item.receiptPreviewUrl || '').trim(), + receipt_source_url: String(item.receipt_source_url || item.receiptSourceUrl || '').trim(), document_fields: Array.isArray(item.document_fields) ? item.document_fields .map((field) => ({ @@ -87,6 +91,87 @@ export function normalizeOcrDocuments(payload) { })) } +function defineFileReceiptId(file, receiptId) { + const normalizedReceiptId = String(receiptId || '').trim() + if (!file || !normalizedReceiptId) { + return false + } + + try { + Object.defineProperty(file, 'receiptId', { + value: normalizedReceiptId, + enumerable: false, + configurable: true + }) + return true + } catch { + try { + file.receiptId = normalizedReceiptId + return String(file.receiptId || '').trim() === normalizedReceiptId + } catch { + return false + } + } +} + +export function attachReceiptFolderIdsToFiles(files = [], payload = null) { + const safeFiles = Array.isArray(files) ? files : [] + const documents = Array.isArray(payload?.documents) ? payload.documents : [] + let attachedCount = 0 + + safeFiles.slice(0, documents.length).forEach((file, index) => { + const document = documents[index] || {} + const receiptId = String(document.receipt_id || document.receiptId || '').trim() + if (receiptId && defineFileReceiptId(file, receiptId)) { + attachedCount += 1 + } + }) + + return attachedCount +} + +export async function collectReceiptFiles({ + files = [], + recognizedAttachmentData = null, + recognizeOcrFiles, + timeoutMs = 90000, + timeoutMessage = '票据 OCR 识别超时,已继续使用附件名称处理。' +} = {}) { + const safeFiles = Array.isArray(files) ? files : [] + const reusedData = recognizedAttachmentData && typeof recognizedAttachmentData === 'object' + ? recognizedAttachmentData + : null + + if (reusedData) { + const ocrDocuments = Array.isArray(reusedData.ocrDocuments) ? [...reusedData.ocrDocuments] : [] + const ocrPayload = reusedData.ocrPayload || { documents: ocrDocuments } + attachReceiptFolderIdsToFiles(safeFiles, ocrPayload) + return { + ocrPayload, + ocrSummary: String(reusedData.ocrSummary || '').trim() || buildOcrSummaryFromDocuments(ocrDocuments), + ocrDocuments, + ocrFilePreviews: Array.isArray(reusedData.ocrFilePreviews) ? [...reusedData.ocrFilePreviews] : [] + } + } + + if (typeof recognizeOcrFiles !== 'function') { + throw new Error('票据采集服务未配置。') + } + + const ocrPayload = await recognizeOcrFiles(safeFiles, { + timeoutMs, + timeoutMessage + }) + attachReceiptFolderIdsToFiles(safeFiles, ocrPayload) + + return { + ocrPayload, + ocrSummary: buildOcrSummary(ocrPayload), + ocrDocuments: normalizeOcrDocuments(ocrPayload), + ocrFilePreviews: buildOcrFilePreviews(ocrPayload) + } +} + export function buildOcrSummary(payload) { return buildOcrSummaryFromDocuments(normalizeOcrDocuments(payload)) } diff --git a/web/src/views/scripts/travelReimbursementConversationModel.js b/web/src/views/scripts/travelReimbursementConversationModel.js index a4f2ca8..c5d6acb 100644 --- a/web/src/views/scripts/travelReimbursementConversationModel.js +++ b/web/src/views/scripts/travelReimbursementConversationModel.js @@ -358,8 +358,9 @@ export function buildExpenseSceneSelectionMessage(rawText) { : '我已识别到这是报销申请。' return [ - `${prefix}但现在还不能确定具体报销场景,所以我先暂停信息抽取。`, - '请先选择本次要发起的报销场景,选择后我再按对应规则继续识别并整理核对信息。' + `${prefix}先选一下这笔费用属于哪一类,我再按对应流程继续。`, + '差旅和业务招待通常需要先关联申请单;交通、住宿、办公用品这类一般可以直接继续填写。', + '选完后我会把下一步需要准备的内容整理给你。' ].join('\n') } @@ -882,6 +883,8 @@ export function normalizeInitialConversationMessages(conversation) { return createMessage(item.role, item.content, attachmentNames, { id: `restored-${item.id || ++messageSeed}`, time: formatMessageTime(item.created_at || item.createdAt), + assistantName: String(messageJson?.assistant_name || messageJson?.assistantName || '').trim(), + assistantVariant: String(messageJson?.assistant_variant || messageJson?.assistantVariant || '').trim(), meta: item.role === 'assistant' ? buildStoredMessageMeta(messageJson, attachmentNames) : [], citations: item.role === 'assistant' && Array.isArray(result?.citations) ? result.citations : [], suggestedActions: @@ -940,6 +943,7 @@ export function serializeSessionMessages(messages) { stewardPlan: message.stewardPlan || null, operationFeedback: message.operationFeedback || null, assistantName: message.assistantName || '', + assistantVariant: message.assistantVariant || '', isWelcome: Boolean(message.isWelcome), welcomeQuickActions: Array.isArray(message.welcomeQuickActions) ? message.welcomeQuickActions : [] })) diff --git a/web/src/views/scripts/useTravelReimbursementSubmitComposer.js b/web/src/views/scripts/useTravelReimbursementSubmitComposer.js index cef21c9..cf00bcd 100644 --- a/web/src/views/scripts/useTravelReimbursementSubmitComposer.js +++ b/web/src/views/scripts/useTravelReimbursementSubmitComposer.js @@ -1,7 +1,8 @@ import { ATTACHMENT_ASSOCIATION_CONFIRM_HREF, buildAttachmentAssociationConfirmationMessage, - buildUnsavedDraftAttachmentConfirmationMessage + buildUnsavedDraftAttachmentConfirmationMessage, + collectReceiptFiles } from './travelReimbursementAttachmentModel.js' import { resolveAssistantScopeGuard } from '../../utils/assistantSessionScope.js' import { @@ -312,8 +313,6 @@ export function useTravelReimbursementSubmitComposer(ctx) { buildExpenseSceneSelectionMessage, buildMessageMeta, buildOcrDocumentsFromReviewPayload, - buildOcrFilePreviews, - buildOcrSummary, buildOcrSummaryFromDocuments, buildReviewFormContextFromPayload, clearAttachedFiles, @@ -348,7 +347,6 @@ export function useTravelReimbursementSubmitComposer(ctx) { messages, nextTick, normalizeExpenseQueryPayload, - normalizeOcrDocuments, persistSessionState, props, recognizeOcrFiles, @@ -1825,23 +1823,28 @@ export function useTravelReimbursementSubmitComposer(ctx) { startFlowStep('ocr', { detail: `正在识别 ${files.length} 份附件...`, startedAt: ocrStartedAt }) } if (recognizedAttachmentData) { - ocrPayload = recognizedAttachmentData.ocrPayload - ocrSummary = recognizedAttachmentData.ocrSummary || buildOcrSummaryFromDocuments(recognizedAttachmentData.ocrDocuments) - ocrDocuments = [...recognizedAttachmentData.ocrDocuments] - ocrFilePreviews = [...recognizedAttachmentData.ocrFilePreviews] + const collected = await collectReceiptFiles({ + files, + recognizedAttachmentData + }) + ocrPayload = collected.ocrPayload + ocrSummary = collected.ocrSummary + ocrDocuments = collected.ocrDocuments + ocrFilePreviews = collected.ocrFilePreviews rememberFilePreviews(ocrFilePreviews) if (!stewardDelegated) { completeFlowStep('ocr', `复用已确认的 ${ocrDocuments.length || files.length} 张票据识别结果`, Date.now() - ocrStartedAt) } } else { try { - ocrPayload = await recognizeOcrFiles(files, { - timeoutMs: 90000, - timeoutMessage: '票据 OCR 识别超时,已继续使用附件名称处理。' + const collected = await collectReceiptFiles({ + files, + recognizeOcrFiles }) - ocrSummary = buildOcrSummary(ocrPayload) - ocrDocuments = normalizeOcrDocuments(ocrPayload) - ocrFilePreviews = buildOcrFilePreviews(ocrPayload) + ocrPayload = collected.ocrPayload + ocrSummary = collected.ocrSummary + ocrDocuments = collected.ocrDocuments + ocrFilePreviews = collected.ocrFilePreviews rememberFilePreviews(ocrFilePreviews) if (!stewardDelegated) { completeFlowStep('ocr', `识别到 ${ocrDocuments.length || files.length} 张票据`, Date.now() - ocrStartedAt) diff --git a/web/src/views/scripts/useTravelReimbursementSuggestedActions.js b/web/src/views/scripts/useTravelReimbursementSuggestedActions.js index 5bb68b2..67e426a 100644 --- a/web/src/views/scripts/useTravelReimbursementSuggestedActions.js +++ b/web/src/views/scripts/useTravelReimbursementSuggestedActions.js @@ -339,6 +339,10 @@ export function useTravelReimbursementSuggestedActions({ const carryText = String(actionPayload.carry_text || '').trim() const carryFiles = actionPayload.carry_files ? Array.from(attachedFiles.value || []) : [] if (!lockSuggestedActionMessage(message, action)) return + if (targetSessionType === SESSION_TYPE_EXPENSE && carryText === '我要报销') { + pushExpenseSceneSelectionPrompt(carryText) + return + } if (String(actionPayload.steward_plan_id || '').trim()) { const confirmedByText = Boolean(action.confirmedByText) delete action.confirmedByText diff --git a/web/tests/ai-application-draft-model.test.mjs b/web/tests/ai-application-draft-model.test.mjs new file mode 100644 index 0000000..4c206b2 --- /dev/null +++ b/web/tests/ai-application-draft-model.test.mjs @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + applyAiApplicationAnswer, + buildAiApplicationStepPrompt, + buildAiApplicationSummary, + createAiApplicationDraft, + getAiApplicationCurrentStep, + isAiApplicationDraftComplete +} from '../src/utils/aiApplicationDraftModel.js' + +test('application draft starts at the reason step', () => { + const draft = createAiApplicationDraft('travel', '差旅费') + assert.equal(draft.expenseType, 'travel') + assert.equal(draft.expenseTypeLabel, '差旅费') + assert.equal(draft.stepKey, 'reason') + assert.equal(getAiApplicationCurrentStep(draft).key, 'reason') +}) + +test('answers advance through fields in order and reach summary', () => { + let draft = createAiApplicationDraft('travel', '差旅费') + draft = applyAiApplicationAnswer(draft, '去上海支持项目部署', []) + assert.equal(draft.stepKey, 'time_range') + draft = applyAiApplicationAnswer(draft, '2026-06-20 至 2026-06-22,出差 3 天', []) + assert.equal(draft.stepKey, 'location') + draft = applyAiApplicationAnswer(draft, '上海', []) + assert.equal(draft.stepKey, 'amount') + draft = applyAiApplicationAnswer(draft, '约 2358 元', []) + assert.ok(isAiApplicationDraftComplete(draft)) +}) + +test('step prompt names the type and the current field', () => { + const draft = createAiApplicationDraft('travel', '差旅费') + const prompt = buildAiApplicationStepPrompt(draft) + assert.match(prompt, /差旅费/) + assert.match(prompt, /事由/) +}) + +test('summary lists every filled field', () => { + let draft = createAiApplicationDraft('travel', '差旅费') + draft = applyAiApplicationAnswer(draft, '去上海支持项目部署', []) + draft = applyAiApplicationAnswer(draft, '2026-06-20 至 2026-06-22,出差 3 天', []) + draft = applyAiApplicationAnswer(draft, '上海', []) + draft = applyAiApplicationAnswer(draft, '约 2358 元', []) + const summary = buildAiApplicationSummary(draft) + assert.match(summary, /差旅费/) + assert.match(summary, /去上海支持项目部署/) + assert.match(summary, /2026-06-20 至 2026-06-22,出差 3 天/) + assert.match(summary, /约 2358 元/) +}) diff --git a/web/tests/ai-expense-draft-model.test.mjs b/web/tests/ai-expense-draft-model.test.mjs new file mode 100644 index 0000000..a90653a --- /dev/null +++ b/web/tests/ai-expense-draft-model.test.mjs @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + applyAiExpenseAnswer, + buildAiExpenseStepPrompt, + buildAiExpenseSummary, + createAiExpenseDraft, + getAiExpenseCurrentStep, + isAiExpenseDraftComplete +} from '../src/utils/aiExpenseDraftModel.js' + +test('draft starts at the reason step regardless of expense type', () => { + const draft = createAiExpenseDraft('transport', '交通费') + assert.equal(draft.expenseType, 'transport') + assert.equal(draft.expenseTypeLabel, '交通费') + assert.equal(draft.stepKey, 'reason') + assert.equal(getAiExpenseCurrentStep(draft).key, 'reason') +}) + +test('answers advance through fields in order and reach the summary step', () => { + let draft = createAiExpenseDraft('office', '办公用品费') + draft = applyAiExpenseAnswer(draft, '项目现场临时采购', []) + assert.equal(draft.stepKey, 'time_range') + draft = applyAiExpenseAnswer(draft, '2026-06-15', []) + assert.equal(draft.stepKey, 'location') + draft = applyAiExpenseAnswer(draft, '京东', []) + assert.equal(draft.stepKey, 'amount') + draft = applyAiExpenseAnswer(draft, '320元', []) + assert.equal(draft.stepKey, 'attachments') + draft = applyAiExpenseAnswer(draft, '稍后上传', []) + assert.ok(isAiExpenseDraftComplete(draft)) +}) + +test('attachments step collects uploaded file names', () => { + let draft = createAiExpenseDraft('office', '办公用品费') + draft = applyAiExpenseAnswer(draft, '事由', []) + draft = applyAiExpenseAnswer(draft, '2026-06-15', []) + draft = applyAiExpenseAnswer(draft, '京东', []) + draft = applyAiExpenseAnswer(draft, '320元', []) + draft = applyAiExpenseAnswer(draft, '', [{ name: '发票.pdf' }]) + assert.deepEqual(draft.values.attachment_names, ['发票.pdf']) + assert.ok(isAiExpenseDraftComplete(draft)) +}) + +test('step prompt names the type and the current field', () => { + const draft = createAiExpenseDraft('transport', '交通费') + const prompt = buildAiExpenseStepPrompt(draft) + assert.match(prompt, /交通费/) + assert.match(prompt, /事由/) +}) + +test('summary lists every filled field and the linked application', () => { + let draft = createAiExpenseDraft('transport', '交通费') + draft = { + ...draft, + applicationClaim: { + application_claim_no: 'AP-202606-001', + application_reason: '送客户去机场', + application_business_time: '2026-06-15', + application_location: '公司至机场' + } + } + draft = applyAiExpenseAnswer(draft, '送客户去机场', []) + draft = applyAiExpenseAnswer(draft, '2026-06-15', []) + draft = applyAiExpenseAnswer(draft, '公司至机场', []) + draft = applyAiExpenseAnswer(draft, '85元', []) + draft = applyAiExpenseAnswer(draft, '稍后上传', []) + const summary = buildAiExpenseSummary(draft) + assert.match(summary, /交通费/) + assert.match(summary, /AP-202606-001/) + assert.match(summary, /85元/) +}) diff --git a/web/tests/ai-sidebar-business-access.test.mjs b/web/tests/ai-sidebar-business-access.test.mjs new file mode 100644 index 0000000..0d757d3 --- /dev/null +++ b/web/tests/ai-sidebar-business-access.test.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { resolveAiSidebarBusinessViewIds } from '../src/utils/aiSidebarBusinessAccess.js' + +test('AI sidebar shows three business entries for regular employees', () => { + assert.deepEqual(resolveAiSidebarBusinessViewIds({ name: '普通员工', roleCodes: [] }), [ + 'documents', + 'receiptFolder', + 'policies' + ]) +}) + +test('AI sidebar adds budget management for budget monitors', () => { + assert.deepEqual(resolveAiSidebarBusinessViewIds({ name: '预算管理员', roleCodes: ['budget_monitor'] }), [ + 'documents', + 'receiptFolder', + 'policies', + 'budget' + ]) +}) + +test('AI sidebar adds finance capabilities for finance users', () => { + assert.deepEqual(resolveAiSidebarBusinessViewIds({ name: '财务负责人', roleCodes: ['finance'] }), [ + 'documents', + 'receiptFolder', + 'policies', + 'overview', + 'audit', + 'digitalEmployees' + ]) +}) + +test('AI sidebar keeps workbench and settings out of the steward business layer', () => { + const viewIds = resolveAiSidebarBusinessViewIds({ username: 'admin', isAdmin: true, roleCodes: ['admin'] }) + + assert.equal(viewIds.includes('workbench'), false) + assert.equal(viewIds.includes('settings'), false) +}) diff --git a/web/tests/ai-sidebar-rail-mode.test.mjs b/web/tests/ai-sidebar-rail-mode.test.mjs new file mode 100644 index 0000000..e6f3156 --- /dev/null +++ b/web/tests/ai-sidebar-rail-mode.test.mjs @@ -0,0 +1,193 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' +import { fileURLToPath } from 'node:url' + +const appShell = readFileSync( + fileURLToPath(new URL('../src/views/AppShellRouteView.vue', import.meta.url)), + 'utf8' +) + +const aiSidebar = readFileSync( + fileURLToPath(new URL('../src/components/layout/AiSidebarRail.vue', import.meta.url)), + 'utf8' +) + +const aiSidebarStyles = readFileSync( + fileURLToPath(new URL('../src/assets/styles/components/ai-sidebar-rail.css', import.meta.url)), + 'utf8' +) + +const aiBusinessAccess = readFileSync( + fileURLToPath(new URL('../src/utils/aiSidebarBusinessAccess.js', import.meta.url)), + 'utf8' +) + +const appStyles = readFileSync( + fileURLToPath(new URL('../src/assets/styles/app.css', import.meta.url)), + 'utf8' +) + +const extractCssBlock = (source, selector) => source.match(new RegExp(`${selector}\\s*\\{([\\s\\S]*?)\\n\\}`))?.[1] || '' + +test('workbench AI mode swaps the traditional rail for the AI three-layer rail', () => { + assert.match(appShell, /import AiSidebarRail from '\.\.\/components\/layout\/AiSidebarRail\.vue'/) + assert.match(appShell, / workbenchMode\.value === 'ai'\)/) + assert.match(appShell, /const isWorkbenchAiMode = computed\(\(\) => activeView\.value === 'workbench' && workbenchMode\.value === 'ai'\)/) + assert.match(appShell, /@new-chat="openAiSidebarNewChat"/) + assert.match(appShell, /@open-recent="openAiSidebarRecent"/) + assert.match(appShell, /@rename-conversation="handleAiConversationRename"/) + assert.match(appShell, /@logout="handleLogout"/) + assert.match(appShell, /import \{ loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation \} from '\.\.\/utils\/aiWorkbenchConversationStore\.js'/) + assert.match(appShell, /function openAiSidebarNewChat\(\)/) + assert.match(appShell, /function openAiSidebarRecent\(item = \{\}\)/) + assert.match(appShell, /function handleAiConversationRename\(payload = \{\}\)/) + assert.match(appShell, /import \{ computed, nextTick, onBeforeUnmount, onMounted, ref, watch \} from 'vue'/) + assert.match(appShell, /async function openAiConversationWorkspace\(type, payload = null\)/) + assert.match(appShell, /const navigation = handleNavigate\('workbench'\)/) + assert.match(appShell, /if \(navigation && typeof navigation\.then === 'function'\)[\s\S]*await navigation/) + assert.match(appShell, /await nextTick\(\)/) + assert.match(appShell, /dispatchAiSidebarCommand\(type, payload\)/) + assert.match(appShell, /void openAiConversationWorkspace\('new-chat'\)/) + assert.match(appShell, /void openAiConversationWorkspace\('open-recent', item\)/) + assert.doesNotMatch(appShell, /openAiSidebarSearchChat/) + assert.match(appShell, /const aiSidebarCommand = ref\(\{ seq: 0, type: '', payload: null \}\)/) + assert.match(appShell, /const aiConversationHistory = ref\(\[\]\)/) + assert.match(appShell, /:active-conversation-id="aiActiveConversationId"/) + assert.match(appShell, /:conversation-history="aiConversationHistory"/) + assert.match(appShell, /:brand-name="PRODUCT_DISPLAY_NAME"/) + assert.match(appShell, /:brand-logo="companyProfile\.logo"/) + assert.match(appShell, /:company-name="ENTERPRISE_DISPLAY_NAME"/) + assert.match(appShell, /:ai-sidebar-command="aiSidebarCommand"/) + assert.match(appShell, /@ai-conversation-change="handleAiConversationChange"/) + assert.match(appShell, /@ai-conversation-history-change="handleAiConversationHistoryChange"/) + assert.match(appShell, /function dispatchAiSidebarCommand\(type, payload = null\)/) + assert.match(appShell, /function handleAiConversationHistoryChange\(payload = \[\]\)/) + assert.match(appShell, /loadAiWorkbenchConversationHistory\(user \|\| \{\}\)/) + assert.match(appShell, /saveAiWorkbenchConversation\(currentUser\.value \|\| \{\},[\s\S]*\.\.\.target,[\s\S]*title/) + assert.match(appShell, /:current-user="currentUser"/) + assert.doesNotMatch(appShell, /restoreLatestConversation:\s*true/) + assert.match(appShell, /'workbench-ai-sidebar-active': isAiShellMode/) + assert.match(appShell, /sidebarCollapsedBeforeAiMode\.value = sidebarCollapsed\.value/) + assert.match(appShell, /sidebarCollapsed\.value = false/) + assert.match(appShell, /sidebarCollapsed\.value = sidebarCollapsedBeforeAiMode\.value/) + assert.match(appStyles, /\.app\s*\{[\s\S]*--sidebar-expanded-width:\s*304px;/) + assert.match(appStyles, /\.sidebar-mode-fade-enter-active,[\s\S]*\.sidebar-mode-fade-leave-active\s*\{[\s\S]*opacity 180ms/) +}) + +test('AI sidebar has quick actions, business navigation and conversation history layers', () => { + assert.match(aiSidebar, /aria-label="AI模式导航"/) + assert.match(aiSidebar, /class="ai-rail-brand"/) + assert.match(aiSidebar, /aria-label="当前产品标识"/) + assert.match(aiSidebar, /displayBrandName/) + assert.match(aiSidebar, /brandName:\s*\{\s*type:\s*String/) + assert.match(aiSidebar, /brandLogo:\s*\{\s*type:\s*String/) + assert.match(aiSidebar, /String\(props\.brandName \|\| '易财费控'\)/) + assert.doesNotMatch(aiSidebar, /远光软件股份有限公司/) + assert.doesNotMatch(aiSidebar, /AI Workbench/) + assert.match(aiSidebar, /aria-label="对话操作"/) + assert.match(aiSidebar, /新建对话/) + assert.match(aiSidebar, /查询对话/) + assert.doesNotMatch(aiSidebar, /自定义/) + assert.doesNotMatch(aiSidebar, /业务工作舱/) + assert.match(aiSidebar, /resolveAiSidebarBusinessViewIds/) + assert.match(aiSidebar, /\.filter\(\(item\) => aiBusinessViewIds\.value\.has\(item\.id\)\)/) + assert.match(aiSidebar, /class="ai-nav-list"/) + assert.match(aiSidebar, /v-for="item in businessNavItems"/) + assert.match(aiSidebar, /ai-nav-copy/) + assert.match(aiSidebar, /item\.aiIcon/) + assert.match(aiSidebar, /aria-current/) + assert.doesNotMatch(aiSidebar, /displayHint/) + assert.doesNotMatch(aiSidebar, /个人工作台/) + assert.doesNotMatch(aiSidebar, /待办与助手/) + assert.doesNotMatch(aiSidebar, /v-html="item\.icon"/) + assert.match(aiSidebar, /最近对话/) + assert.match(aiSidebar, /conversationHistory:\s*\{ type:\s*Array,\s*default:\s*\(\) => \[\] \}/) + assert.match(aiSidebar, /const conversationSearchOpen = ref\(false\)/) + assert.match(aiSidebar, /const conversationSearchQuery = ref\(''\)/) + assert.match(aiSidebar, /function openConversationSearch\(\)/) + assert.match(aiSidebar, /\s*