New color×variant×size Button API (solid/outlined/filled/text/link) with legacy shadcn variants auto-mapped; btn-* semantic tokens + touch hit-area in style.css/tailwind; app-wide hoverOnlyWhenSupported; gallery ButtonSection; migrate KnowledgeSpaceSidebar create button to the new API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-layer type tokens (primitive --font-size-N + semantic --text-*) in style.css with a mobile remap, wired to Tailwind fontSize; system font stack; new gallery TypographySection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sync_information_article iterated channel_info_source (a tenant-aware table)
with no tenant context set, so on a multi-tenant deploy the first SELECT raised
NoTenantContextError and the daily article sync never ran — subscribed sources'
content never updated.
Mirror reconcile_all_tenants: enumerate active tenants (root + active children,
or the default tenant when multi-tenancy is off) and run the sync under each
tenant's context, isolating per-tenant failures. Add a sync counterpart
TenantDao.get_children_ids_active for the sync worker path, and fix a latent
missing `import asyncio` in the knowledge-space dispatch hook.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to 1208fa158: instead of only relaxing the pattern to allow the
empty string, remove it entirely to align with feat/2.6.0 (72fd1e8f0
"fix: unused pattern"). The whitelist could not stop XSS (it allowed <>/"'&)
yet rejected the empty default plus emoji / non-CJK text; real escaping is
done by the frontend (React text nodes, no dangerouslySetInnerHTML). Keeping
all three branches identical avoids a future merge conflict on these lines.
Update regression tests accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
applicationCenterWelcomeMessage / applicationCenterDescription were declared
with default="" but a pattern requiring >=1 char (^[...]+$). Any tenant whose
stored workstation config left these fields empty or unset (the default) made
WorkstationConfig(**raw) raise a pydantic ValidationError, so GET
/api/v1/workstation/config returned 500 and the client rendered its full-screen
"system maintenance" overlay — /workspace/c/new appeared broken.
Relax both patterns from + to * so the empty string is accepted while keeping
the character whitelist for non-empty input. Add regression tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The task result panel reads its liked verdict from the linsight store, which is
seeded once on hydration. An optimistic like updated only the button's local
state + backend, not the store — so switching away and back re-mounted the panel
with the stale store value and the highlight vanished (a full page reload fixed
it, since that re-hydrates the store from the server).
Sync the verdict back to the store on click via a new onLikedChange callback
(TaskTurnPanel + ExecutionFlow pass updateLinsight), so the store stays current
and the highlight survives switch-away/switch-back.
In task mode the result panel's like/dislike showed as soon as the run hit
'completed', but the task turn still carried its streaming placeholder id
(userMessageId + '_') — the real persisted category="task" ChatMessage id only
arrived on a page reload. A like clicked the moment the panel appeared wrote to
a non-existent row and silently vanished (occasional, timing-dependent).
Source the feedback id from the linsight store instead of the conversation
message, and populate the store's real message_id at completion by reusing the
existing session-version-list enrichment (no new endpoint/field):
- Websocket final_result: fetch the version list, copy this version's real
message_id + liked into the store
- TaskTurnPanel: ResultPanel reads linsight.message_id (history hydration
already seeds it via the same endpoint); add allowFeedback prop for the
share-view opt-out
- AiMessageBubble: pass allowFeedback instead of encoding it via messageId
The button now appears only once the real id is in the store, so show-time and
id-availability are aligned and the placeholder can never reach /liked.
update_workbench_llm persisted the incoming WorkbenchModelConfig wholesale, so a
body without `models` (Pydantic defaults it to None) nulled the entire configured
dialogue-model list — the way a stale admin page wiped the Root config in prod.
Treat a missing/None `models` as "no change" and keep the stored list; an explicit
`models: []` still clears. asr/tts/chat_title keep their None-clears semantics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A blank dialogue-model row ({id:''}) saved from the workbench model config
flows into the workspace model pickers as <SelectItem value="">, which Radix
forbids — it throws and crashes the whole /workspace/c/new page.
- client: drop empty/invalid-id options in AiModelSelect and the Linsight
ModelSelector so a stale/blank model never renders an empty-string
SelectItem value (defense that doesn't rely on backend sanitizing).
- platform: reject saving a blank model row in WorkbenchModel — validation
only checked models.length, letting {id:''} through and overwriting the
configured dialogue models with an empty row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Channel article chat yielded the 'end' event BEFORE persisting the answer
ChatMessage, so the client never got the real id — the streamed answer kept its
temporary placeholder id and a like clicked before switching away wrote to a
non-existent row and vanished (same class of bug as the knowledge-space fix).
Persist the answer first, then emit the end event with its message_id so the
client can swap the placeholder out immediately (frontend useChannelChat already
consumes it).
A TTS provider failure (e.g. empty audio) raised a bare exception that
propagated as HTTP 500, which the client's global interceptor treats as a
service outage and redirects to the maintenance overlay — a poor experience
for what is just 'this one utterance failed to synthesize'.
- backend: wrap the synthesis call, raise a dedicated TtsSynthesisFailedError
(code 10026) instead of letting it bubble up as 500
- frontend: textToSpeech opts into the interceptor's translate-and-toast path
(skip403Redirect) instead of the 500 escape hatch; TextToSpeechButton no
longer masks the real error or double-toasts once the interceptor already
showed the localized message
- add api_errors.10026 to zh-Hans/en/ja
Knowledge-space chat (single-file / folder) rendered the streaming answer under
a temporary client-side placeholder id (userMessageId + '_') and never swapped
it for the real persisted ChatMessage id, so a like clicked before the first
page reload wrote to a row that never existed — it silently vanished. Reload
loaded the real id from history and worked fine, masking the bug as 'only
fails right after sending, fixed by refresh'.
- backend: the 'end' stream event now carries the persisted answer message_id
- frontend: useStreamChatSSE forwards it through onFinal; useFolderChat /
useFileChat / useChannelChat swap their placeholder messageId for it so
like/dislike (and any other per-message action) targets the right row
immediately, not just after a reload
Add a shared CommentDialog (optional-comment / reason dialog) + gallery FeedbackSection; migrate the message 点赞/点踩 feedback (dropping the bespoke MessageFeedbackForm) and the menu-unavailable apply-reason dialog onto it; feedback comment is now optional (com_feedback_placeholder, en/zh/ja).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The linsight task result is already persisted as a category="task" ChatMessage
in the unified daily conversation, so rate it through the shared chatmessage
feedback (/liked + /chat/comment) instead of a linsight_session_version-specific
column/endpoint. All four AI Q&A surfaces now share one storage + rollup path.
Backend:
- chat_helpers: agent history formatter returns liked + remark (fixes daily +
in-conversation task-turn highlight on reload)
- workstation_schema: WorkstationMessage carries liked + remark (channel surface)
- linsight utils: get_task_feedback_by_version maps session_version -> its task
ChatMessage {message_id, liked}
- linsight endpoint: session-version-list enriches each version with message_id +
liked for the standalone linsight page
Frontend:
- ResultPanel: rate by messageId via likeChatApi / disLikeCommentApi
- TaskTurnPanel forwards messageId + liked; AiMessageBubble passes the task row's
message.messageId (hidden on the read-only share view); ExecutionFlow reads them
from the enriched session-version store field
- drop the now-unused likeLinsightVersion / commentLinsightVersion
The PRD doc is no longer needed; the 点赞/点踩 feature itself (98b48c93a) is
unaffected — only its now-redundant design doc is removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten padding (px-6 -> px-5), add a focus ring (border + 2px shadow) on the tag input, disable autocomplete, and normalize placeholder / no-tags text to #999.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swap the touch-mobile: variant for max-md: so the edit-tags dialog uses the standard breakpoint-based mobile full-screen sheet (rounded corners only from md up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace OGDialog/OGDialogTemplate delete confirmations in SharedLinks and the standalone guest-convo item with useConfirm; touch up the ConfirmDialog + Modal gallery sections.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Root-list loading no longer depends on currentFolderId, so selecting a
folder (click or route change) never rebuilds this or any other space's
tree; switching the active space leaves background trees untouched.
- Clicking a folder row (or a space row) only selects/navigates; expanding
and collapsing is exclusively the chevron's job.
- Deep links (direct URL into a nested folder) still auto-expand the
ancestor chain, fetching only levels not already cached, gated on the
root list being genuinely loaded (fixes the reload race that left the
tree half-expanded).
Product PRD (pyramid structure) for adding thumbs up/down feedback to the
four AI Q&A surfaces (workstation daily, linsight task mode, knowledge-space
知源, channel subscription). Feedback persists on the answer row and rolls up
into message_session for single-table, per-surface analytics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add useContainerCompact (ResizeObserver on the flex-1 toolbar column) + TOOLBAR_COMPACT_THRESHOLD; the daily/task input toolbars now collapse button labels to icons based on the actual space available (correct when the sidebar opens) instead of a viewport media query, sized for the widest (English) locale so labels collapse as a group before any single one truncates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete the 22 client test files using jest.mock() (flagged by a customer
scan). Not wired into CI; suite to be reorganized later. No production code
references them.
Give the description input more room (ModelManagement grid 1fr -> 1.3fr, WorkbenchModel container 640 -> 720px) and shorten modelDescriptionPlaceholder (en/zh/ja).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Interactive SSO login-sync never received a department for OAuth/LDAP
logins: the gateway sends identity only, and department binding is a
WeCom-only batch org-sync that is off by default. First-login users were
therefore left with no department (blank in the member picker), unlike
self-registered users who join 临时访客.
Mirror self-registration by reconciling guest-department membership on
login (invariant: a user is in the guest department iff they have no
other department). Orphan users join BS@guest as primary; the placeholder
is vacated once a real department exists. Guest is a local department
under the root tenant, so the leaf tenant stays ROOT — existing
department-less users keep their current tenant. Runs after the
account-disabled short-circuit so disabled placeholder users are skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the maintenance illustration from database-with-wrench to magnifier-over-a-bug (6 greens mapped to illus-500/300/100 by lightness, grey-mode aware); soften the overlay copy to a friendlier tone (en/zh/ja); update the illustration table in BRAND-THEME-HANDOFF.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a created/joined section is empty (and not the compact dropdown), stretch it to fill the remaining list height and vertically center the no-data text (both empty → 50/50); no-data color #818181 -> #999999.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show a spinner while the first file-list page loads (no empty-state flash), guard against out-of-order responses on space/folder switch with a request-seq ref, and use EmptyStateIllustration / LoadingIcon for the empty and loading states.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap SelectPrimitive.ItemText in a min-w-0 flex-1 div (Radix ItemText drops className) so long model name+description truncates before the pr-8 indicator instead of overflowing; drop both model pickers' max-width 280 -> 240.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch the picker item from a stacked name/description column to a single row: name + a 1px divider + truncated description, so options stay one line tall.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After html/body scrolling was globally disabled (WebView bottom-strip fix), these mobile pages' h-full collapsed under the h-auto MainLayout shell and nothing scrolled; pin them to 100dvh (<=767px) so their own overflow-y-auto becomes the scroller.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Admins can set a one-line description (WSModel.description, max 50) per workbench model; the client model pickers (AiModelSelect / linsight ModelSelector) render it under the name, hidden when empty. Platform ModelManagement/WorkbenchModel gain the input + modelDescription(Placeholder) copy (en/zh/ja).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Custom-answer placeholder grows the Shift+Enter newline hint once the row is active (new com_linsight_clarify_custom_active, en/zh/ja); always font-normal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ModelManagement shows the resolved model name as the display-name placeholder; WorkbenchModel save maps empty displayName to the model's cascader label so the workspace model picker never renders a blank option.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swap the custom-answer input for an auto-growing textarea: Shift+Enter inserts a newline (plain Enter still confirms+advances, IME-guarded), height re-fits to the current question on page change (the DOM node is reused), and the number pins to the first line / 确定 to the bottom-right as it grows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Users pasting an api_key with a trailing space into the model provider
config caused every LLM call to fail with APIConnectionError, because
httpx/h11 rejects "Bearer sk-xxx " as an illegal Authorization header.
Sanitize key/secret/url/endpoint fields in LLMService on both create
and update so the stored config is always clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The space children/search endpoints no longer return the per-folder file_num
(an expensive recursive count). Folders now expose a lightweight has_failed_files
boolean (computed from the same status-grouped query, true when a child is
FAILED/VIOLATION) which drives the "retry" affordance precisely.
Client: folder rows drop the "x个项目" count and simply show the folder's most
recent update time; folder retry gating (single + batch) now uses has_failed_files
instead of successFileNum < fileNum. The actual retry stays fully backend-driven.
session-version-list unconditionally narrowed the version list to
meta_data.versionId after the shared_to_session grant. A whole-conversation
(workbench_chat) share carries no versionId, so the filter collapsed the
list to empty and non-owner viewers saw "任务详情加载失败" while the owner
(user_id match, bypass branch) always worked.
In 2.6 task mode the 2.0 integer-"version" re-run concept is dead (version is
now a datetime sort key, one session_version per turn), so a whole-conversation
share must return every turn's version. Narrow only when the share explicitly
pins a versionId (single-version linsight_session share); also guard
meta_data=None against a 500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sharpen the earlier 'schema vs script' guidance into a hard rule: alembic
revisions may contain only DDL; the sole permitted data effect is a
server_default fill on an added column. Any read-then-write on existing rows
(SELECT->UPDATE/INSERT, INSERT...SELECT, dedup/backfill/cleanup/reconcile) is a
separate operational procedure, never in a revision — including cleanup needed
before a constraint (run as an ops prerequisite; the DDL fails loudly if data
wasn't prepared).
Removes the prior 'not part of a schema change' loophole. Notes existing
revisions (f001, f011 dedup, f035, ...) as grandfathered. Motivated by the
100k-user case where inline SELECT-then-UPDATE / per-row Python loops stall the
(now fail-fast) startup migration.
Updates both src/backend/AGENTS.md and the alembic AGENTS.md (Sec 5 + checklist).