The stream, the DB and the grouped event array all carry a truthful
interleaving — the backend closes the open thinking segment before every tool
call precisely so each ReAct round can stand on its own. The final render then
threw that away: two filter() calls merged every thinking fragment into one
block at the top and dropped all tool rows below it, so reasoning that happened
after a search read as if it happened before.
Replace the filters with an in-order walk that folds only ADJACENT thinking
fragments (task mode's DeepStepGroup algorithm), leaving each tool call where
it occurred; the rail connector now follows "is there a next row" regardless of
kind. Adjacent rounds join with a paragraph break — they are complete passages,
not per-frame chunks. A just-opened live segment with no text yet is skipped so
the connector never points at a row that renders nothing. In-place completion
updates and collapse/reopen were already order-safe; backend untouched.
The 7% brand-alpha tint let the white below bleed through, reading a
touch dimmer than the design's selection fill. blue-50 is the same hue
as an opaque swatch — still a brand token, so it keeps following the
blue⇄green theme. Same change as cofco's fbcd7adc8, applied to the
main line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The attachment row orders chips by when each one first appeared, newest
leftmost. Knowledge selections are stored newest-first (the picker prepends)
while the file arrays are oldest-first, so when a whole set first appeared in
one frame — restoring a conversation from localStorage — the newest space was
handed the lowest sequence and the row came back reversed. Picking spaces one
at a time had always been right, which is why the two disagreed.
Feeding the knowledge array in oldest-first makes the sequence mean the same
thing for every source. File and skill chips are untouched.
One-off cleanup for rows the previous commit's bug left behind: media
knowledgefile rows marked SUCCESS whose stored transcript is empty or
the raw ASR response envelope.
The script scans media-extension SUCCESS rows, downloads each file's
MinIO transcript preview (preview/{id}.md) to judge the ingested text,
and converges hits to the same terminal state as every other
unrecognizable-audio case: Milvus/ES vectors deleted (MinIO source file
kept for re-parse/download), status=FAILED, remark=10956 error json —
the client then shows the 失败 badge with reason and stops the row
being clickable. Dry-run by default, --apply to write, scopable via
--knowledge-id / --file-id; idempotent since converged rows drop out of
the SUCCESS selection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When Aliyun ASR answered 200 with no recognized sentences
(output: null), the fallback did str(result) — stringifying the whole
RecognitionResult — so the file was ingested with
'{"status_code": 200, "request_id": ...}' as its transcript and marked
SUCCESS. Silent media looked successfully transcribed, with JSON
garbage as the recognized text.
The fallback now only accepts an explicit plain-text field on the
output payload; with nothing there it returns no segments, which flows
into the existing no-recognizable-audio failure (10956) like every
other silent-media case. Tests cover both the envelope case and the
legitimate output.text fallback.
Existing dirty rows are converged by a follow-up script (next commit).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The empty-state stretch logic made created/joined sections flex-grow
whenever EITHER was empty, so collapsing or filling one section moved
its neighbours around. Now the list always fills its scroll viewport
and only the LAST section (joined) absorbs the leftover height:
- Upper sections keep a fixed 96px empty-state slot, so nothing above
ever shifts — collapsing only pulls the content below upwards.
- Joined keeps flex-1 with its automatic min-height, so a long list
still grows past the viewport and scrolls instead of being squeezed.
- The three duplicated empty-state blocks fold into one renderEmptyState.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1bb87ba22 renamed the sidebar entries but each module still greeted the
user with its fixed i18n title — configure 订阅 as "资讯" and the page
banner still shouted 订阅. The in-module surfaces now read from
useWorkbenchMenuNames too: the subscription banner/switcher/sidebar
headings and dialog aria-labels, the app-center page title and mobile
header, and the knowledge-space sidebar/drawer titles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four client workbench entries (首页 / 知识空间 / 订阅 / 应用) were
fixed i18n strings; deployments wanting their own wording had no knob.
Each module's platform config tab (构建 → 工作台) now carries a required
菜单显示名称 field, and the client sidebar renders whatever was saved.
- Backend: home/app-center names ride on WorkstationConfig (they share
the daily config record); knowledge-space and subscription get a
menu_display_name on their own config models, surfaced through
/workstation/config alongside assistant_name.
- Platform: shared clampMenuName truncates as you type by DISPLAY width
(full-width chars count 2, cap 8 units — 4 CJK chars or 8 latin),
since the sidebar column is 64px at 10px type; saving an emptied field
is rejected rather than silently falling back.
- Client: new useWorkbenchMenuNames hook resolves each entry to the
admin value or the localized default (blank = unconfigured, so
deployments that never touch it still follow the UI language);
consumed by the hub tabs, sidebar, mobile drawer and the
menu-unavailable page.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
部门知识空间 only rendered once the user actually had one, while 我创建的 and
我加入的 always render and show 暂无数据 when empty. That made the category look
like a search result rather than part of how the page is organised — a user with
no department space had no way to tell the grouping existed at all.
It now behaves like its two siblings, in both the sidebar and the file-page title
dropdown. Auto-selecting the first department space still requires there to be
one, and the empty-section stretch stays with 我创建的 / 我加入的 so the existing
layout is unchanged.
The picker merged 部门 / 我创建的 / 我加入的 into one flat A–Z list, so a user
could not tell where a space came from. It now shows the same three sections,
in the same order, as the 知识空间 page — grouping rules are not re-derived: a
new useCategorizedKnowledgeSpaces hook reuses that page's endpoints, query keys
and department-subtraction dedup, so the two surfaces cannot drift and opening
the picker after visiting the page serves from cache. A category with nothing to
show hides its title, which also covers search: hits stay under their own
section and sections with no hit disappear.
The list panel moves out of ChatKnowledge into its own file and grows a grouped
mode; organization knowledge bases keep the flat, server-paginated one.
Reaches daily chat and the task-mode landing (both render AiChatInput).
Selection, the 50-per-type cap and the chips above the input are untouched.
Pressing Enter only set the search keyword, so re-submitting the same link left
every dependency identical and the effect that resolves it never re-ran: the
user was told to fix the link and press Enter, and pressing Enter did nothing
unless they actually edited it. The in-flight guard compounded it by keying on
the link itself rather than the attempt.
Enter now goes through a submit that counts attempts, and the guard keys on the
attempt — a resubmission is a retry, a re-render mid-flight still isn't. The
failure copy also clears on the keypress instead of when the debounced request
finally starts.
Both hint sentences stay on one line.
Searching for an unindexed account left the user with a two-line explanation
and nowhere to go, even though the very next thing they need — where to copy an
article link from — was already one hover away on the failure screen.
The hint component now takes its own copy, so both moments share the tooltip
while wording the advice for their own situation, and the empty-search copy
collapses to a single line naming the article link as the way forward.
Drops source_not_indexed_try_full_url, the fallback the old copy read from,
which no longer has a caller.
Navigating to any existing conversation reset the task-mode toggle to
off, so leaving a task conversation and coming back dropped it (the
execution panel survives via its own by-SV hydration; only the toggle
was lost). Detect a task conversation from its loaded history — guarded
by the message's own conversationId so the stale previous-conversation
message list that lingers for one commit during the navigation
transition can't misfire — and re-enter task mode; a daily chat stays
off, and an explicit manual toggle-off within a visit is preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 0327a05ebe88fbda261d689c1dcbdf7118e999d7)
When a pasted WeChat article URL cannot be resolved, the old flow popped a
hand-rolled "channel add failed / try again" dialog on top of the generic
red error toast, and left the panel with no next step.
Now a single acknowledge-only confirm ("back to edit") explains the failure,
and the processing panel swaps its copy for retry guidance: the highlighted
"WeChat link" phrase carries a tooltip with a screenshot of where to copy a
valid link from. The sentence ships as one i18n key with a {{link}}
placeholder so translators keep control of word order.
Supporting changes:
- ConfirmContext: hideCancel option for dead-end notices, plus a fixed h-8
button height instead of h-auto + py.
- Tooltip2: arrowClassName so the arrow can follow a recoloured panel.
- addWechatSourceApi: showError off, the caller owns the error UI now.
- packages/ui docs: document the hideCancel variant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A message the user just sent isn't in the database yet, so asking the backend
to re-issue a link came back empty — workflow chat showed "no longer
available", task mode sat in its skeleton — and both only came right after a
refresh. The upload already handed the client a working link, so that renders
immediately and the re-issued one takes over when it arrives, which is what an
older conversation needs.
Stored links carry the internal storage host, so they get the same rewrite the
download card does before being handed to the browser.
History rebuilt each attachment as just a name and a url, so the id an image
is resolved by — and the object name behind it — never reached the bubble: a
refreshed conversation showed the picture stuck loading forever. It now spreads
the stored entry instead of listing fields, which also keeps whatever the
backend adds next.
Live sends hand the bubble an array, but history returns what the message row
holds — a JSON string — so every field read off it came back undefined and the
image sat in its skeleton after a refresh. Both shapes are accepted now.
Two reasons a workflow-chat image resolved to nothing, both silent:
The promotion ran under `if not chat_response.is_bot`, but that field defaults
to True, so the user's own question — the only message this was meant for —
was the one case it skipped. Keyed on the category instead.
And it looked for the upload link under `filepath`/`file_path` only, while
workflow chat carries it as `file_url`, so even when it did run there was
nothing to copy. All three spellings are accepted now.
Refs: features/v2.6.0/043-chat-file-permanent-storage
Square the attached-image thumbnail at 100x100 (was 160x120) and give it a
fixed light-gray backdrop so a transparent PNG still shows its artwork —
fixed rather than a neutral token, since the artwork inside the file does
not invert with the theme.
The expired placeholder follows the thumbnail it stands in for: same size,
same tint, no border, lighter icon and label, and the Chinese copy breaks
where it reads best instead of wherever 100px happens to fall.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Workflow chat never carried its attachments as data — filenames were glued
onto the question text and the message's files array stayed empty, which is
why images rendered as a line of text no matter which component drew them.
Attachments now travel alongside the existing dialog_files_content the input
node reads: the client sends them structured, the callback stores them on the
question message, and the bubble renders images as images. Because the message
holds them, they also survive a refresh and go through attachment promotion
like the other chat modes.
The names stay prepended on the way to the workflow node, which still relies on
them; the bubble drops them from the displayed text so they aren't shown twice.
User attachments also stop being routed to MessageFile, which is for files a
run produced.
A just-sent image showed "image is no longer available" for a moment before
appearing: the component treated a missing conversation id as a dead file, but
a message that hasn't been persisted yet simply doesn't have one. Absent ids
now mean "still waiting" and hold the skeleton.
Workflow chat rendered every attachment as a plain filename. Its socket handler
rebuilds each file with only name and url, dropping the id — which is what an
image is looked up by when its link is re-issued. The id is carried through now.
The resolved link and the ids it came from are also exposed as data attributes,
so a picture that fails to render can be traced from devtools.
Parse the transcript into timestamp/text cues so the time reads as a caption-sized label above each line instead of running inline with the sentence; lines without a leading timestamp continue the cue above, and text with no cue at all falls back to plain markdown. Also align the segmented control with the transcript inset, clear the AI dock with bottom padding, and round the player to 12px.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Attachments that are pictures now render as thumbnails that open full screen,
in daily, task and workflow chats alike — until now every attachment was a
filename or a download card, so users could not tell what they had sent.
The link is fetched fresh at render rather than reusing the one stored on the
message, which was signed at upload time and has expired on any older
conversation. When the file genuinely can't be retrieved — cleared storage, or
a message from before attachments were kept — a placeholder says so, instead
of a broken image the user would take for a loading failure.
Whether an attachment is an image is decided from its filename: older messages
carry no MIME type and the upload endpoints disagree on where they put it,
while the name is always there and already drives the file icon.
Refs: features/v2.6.0/043-chat-file-permanent-storage (T010-T013)
Links issued at upload time expire, so the client now asks for a new one when
it renders an attachment. Which object gets signed is decided from the
conversation's own messages and never from anything the caller sends — an
endpoint that signed a caller-supplied object name would hand out the whole
bucket to anyone with an account. Ownership of the conversation is the entire
authorization story; attachments recorded before this feature have no object
name and read as gone rather than falling back to a guess.
Deleting a conversation now drops its files. The delete is soft with no way
back, so nothing is left to read them. Cleanup is best-effort: a storage
outage must not leave the user with a conversation that refuses to disappear.
Refs: features/v2.6.0/043-chat-file-permanent-storage (T007-T009)
Wires promotion into the four places a chat message is persisted, so files
stop vanishing with the temp bucket's 3-day sweep.
Daily chat now uploads through the shared endpoint instead of its own: that one
already names objects by uuid, which removes the collision where two users
uploading "1.png" overwrote each other and could see each other's file. The old
endpoint stays in place for any caller we don't know about.
Workflow chat promotes only the user's own attachments — what the workflow
itself produced is a separate concern. Task mode turned out to be a third
architecture entirely (uploads land on local disk and only reach object storage
at execution time), but its ingestion already persists original image bytes for
the workspace preview, so the conversation just records that object name.
Promotion is best-effort throughout: unreachable storage or a failed copy costs
a thumbnail, never the message.
Refs: features/v2.6.0/043-chat-file-permanent-storage (T003-T006)
Chat uploads land in a temp bucket that wipes itself every 3 days, so a
conversation's files quietly disappeared. Rather than making every upload
endpoint write somewhere permanent, the message-send path now copies its
attachments into the main bucket and records the permanent object name.
Promoting on send keeps the shared /knowledge/upload endpoint — used by the
knowledge base, dataset creation and the API examples — completely untouched,
and abandoned uploads need no cleanup machinery of their own: a file that is
never sent stays in temp and expires there.
The copy is server-side, files that are already permanent are skipped so
task-mode uploads flow through unchanged, and one failed attachment neither
blocks the message nor the other files.
Refs: features/v2.6.0/043-chat-file-permanent-storage (T001-T002)
The design had attachments under chat/<chat_id>/ so deleting a conversation
could sweep one prefix. That can't work: the first upload of a new chat happens
before the chat exists — the client still holds "new" and the backend assigns
the id when the message is sent — so there is no conversation id at naming
time. Group by uploader instead and let deletion collect object names from the
conversation's messages, which already carry them.
Also records that /knowledge/upload is shared with the knowledge base, QA
import and dataset flows, so the workflow-chat path needs its own endpoint
rather than a change to that one.
Refs: features/v2.6.0/043-chat-file-permanent-storage design §3 decision 1, §5 pits 8-9
Groundwork for making chat attachments outlive the 3-day temp bucket. Two
pieces, both with the same motivation: files are about to stop expiring, so
naming and cleanup have to be right before that happens.
Object names become chat/<chat_id>/<uuid><ext>. Daily-mode uploads were stored
under the raw filename, so two users uploading "1.png" overwrote each other —
invisible while the bucket was wiped every 3 days, a permanent data leak once
it isn't. Only the suffix is ever read from the client-supplied name, so a
filename can't steer the object elsewhere in the bucket.
Prefix deletion is what lets deleting a conversation drop its files in one
sweep. It keeps going when an individual object fails — one unreachable file
must not strand the rest — and refuses a prefix that would match the whole
bucket, since no caller wants that and the blast radius is everything.
Refs: features/v2.6.0/043-chat-file-permanent-storage (T001-T002)
Icon spec: new FigDash figure showing why dash/gap must be set at the 24px display size instead of reusing large-size values; add the 'no mask' drawing rule (mask/clipPath need unique ids that collide when icons are inlined together, and they interfere with currentColor); tidy FigRhythm shapes and the §3 wording.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the browser-native audio/video controls (whose icons and overflow menu can't be styled) with a hand-rolled control bar over the standard HTMLMediaElement API: play/pause, seek, volume, playback rate, PiP, fullscreen and download. Ships zh-Hans/en/ja copy for every control, and bumps bisheng-icons to 0.2.28 for the Filled/Outlined glyphs it uses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The feat/2.6.0 merge brought in its own MEDIA_FILE_EXTENSIONS while 146c7c5ab had added an equivalent one lower in the file; git took both, so esbuild failed with 'Multiple exports with the same name'. Keep the original position (ALLOWED_EXTENSIONS uses it above) and move AUDIO_/VIDEO_FILE_EXTENSIONS up beside it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump bisheng-icons to 0.2.24 for Outlined.FileAudio / FileVideo, then use them: FileTable's name cell gains a renderRowFileIcon helper (audio / video / image / generic) and MoveToDialog's right-panel glyph matches. Extract AUDIO_FILE_EXTENSIONS / VIDEO_FILE_EXTENSIONS in knowledgeUtils so both call sites and MEDIA_FILE_EXTENSIONS share one list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the catch-all 'media' key into 'audio' (FileAudio, orange #FF9908) and 'video' (FileVideo, purple #A140FF), and add 'html' (FileHtml, sharing the txt slate-grey palette) — each with its own gradient backdrop and extension mapping. Web-link files now render FileHtml instead of the md icon, and html joins the no-thumbnail text formats.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump both declarations (client package.json + the pnpm-workspace catalog used by @bisheng/ui) and the lockfile. Remember to clear node_modules/.vite after pulling — its git-source exports defeat Vite's dep-change detection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flip the media / webLink upload capability switches on; RichKnowledgePreview media view is now side-by-side (player left, transcript right, single divider), the transcript tabs become a blue-500 segmented control, and the shared MarkdownBody is extracted from MarkdownBlock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Org-KB / task-mode / add-skill / feedback-check active colors switch from blue-600 to the blue-500 brand token, matching the other input-bar selectors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task-mode entry points (PlusMenu, TaskModeToggle, NewChat new-task button, ChatKnowledge) now use Outlined.ListSuccess instead of Outlined.Binoculars; colors/classes unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
localStorage 'fontSize' is written as a plain class string ('text-base'), but startup read it back with JSON.parse — JSON.parse('text-base') throws 'Unexpected token e', crashing ThemeProvider and blanking the whole app on any load where the value is present (StrictMode double-invoke in dev, or any second visit in prod). Read defensively: accept a plain string and tolerate a legacy JSON-encoded value, never throw. Pre-existing bug (2025-04), also on feat/2.6.0-beta4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- orgSyncStore deleted (single-page state, moved into the OrgSync component)
- assistantStore: loadAssistantState (fetch+set) -> pure setAssistantDetail;
4 call sites fetch via getAssistantDetailApi, bare calls now wrapped with
captureAndAlertRequestErrorHoc
- diffFlowStore: version fetching + SSE compare orchestration extracted to
pages/DiffFlowPage/useDiffFlowRun; store keeps pure state transitions
- DiffFlowPage component renamed from lowercase `index` (clears 8 suppressed
rules-of-hooks violations); editAssistant imports useLocation from
react-router-dom (transitive react-router copy broke Router context)
Only the primary landing path (login + main layout + chat home) stays in the
entry chunk; 13 pages (knowledge, subscription, linsight, apps, share,
standalone chat, previews, ...) now lazy-load on first navigation with a
route-level Suspense fallback. Entry main.js: 1.6MB -> 959KB.
- new api_errors `fallback` key (3 languages); naked i18n keys can no longer
reach users when a code has no translation (gateway codes, pre-CI legacy)
- client (6 exits): SSE/WS/chat-input/route/request-layer fall back to generic
Chinese; t(key, vars) kept so {{xxx}} templates still interpolate
- platform (5 exits): fall back status_message -> generic copy (admin keeps
backend diagnostics); chatErrorMessage's literal "error" fallback removed
Auto table layout sized comparison columns by content, so the longer example took the wider column and the two peers read lopsided. Force table-layout:fixed for two-column tables whose body holds plain prose only — label+spec tables (which put code/strong in a long right column) still need content-based sizing and are excluded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
元-文档撰写规范 gains §0 (who each doc is for: designers/PMs, collaborating devs and their agents, the designer's own agent — hence a reader-facing part and an agent-facing hidden zone; technical detail, ledgers and decision history belong in the hidden zone). Conform the affected docs: 文案规范 drops the half-width-digits rule and renumbers, 滚动条规范 switches to Arabic section numbers and marks its leftover-exceptions/acceptance sections site-hide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>