Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/dist/roundtable.css

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions assets/dist/roundtable.js

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions assets/src/community-app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ export function CommunityApp() {
setTabCounts(counts);
}, []);

async function confirmPublish({ title, summary }) {
async function confirmPublish({ title, summary, type }) {
if (!publishDraft?.id || publishBusy) return;
setPublishBusy(true);
const res = await publishCase({ case_id: publishDraft.id, title, summary });
const res = await publishCase({ case_id: publishDraft.id, title, summary, type });
setPublishBusy(false);
if (res.error) return;
setPublishDraft(null);
Expand Down Expand Up @@ -118,7 +118,7 @@ export function CommunityApp() {
<StartedList
refreshNonce={listRefresh}
onSelectTopic={openTopic}
onReviewDraft={(topic) => setPublishDraft(topic)}
onReviewDraft={(topic) => setPublishDraft({ ...topic, summary: topic.snippet })}
onVoteChange={refreshTabCountsOnly}
/>
)}
Expand Down Expand Up @@ -153,7 +153,10 @@ export function CommunityApp() {
aria-label="Close chat"
onClick={() => setChatOpen(false)}
>×</button>
<ChatPanel resetNonce={resetNonce} onTopicPublished={bumpLists} />
<ChatPanel
resetNonce={resetNonce}
onTopicPublished={() => { bumpLists(); setActiveTab('started'); }}
/>
</aside>
{publishDraft && (
<PublishDialog
Expand Down
16 changes: 11 additions & 5 deletions assets/src/components.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export async function sendTurn(text, {
setTurns((t) => {
if (!t.length) return t;
const last = t[t.length - 1];
if (last.reply === '' && last.finalText) {
if (last.finalText && last.finalText.length > (last.reply || '').length) {
const next = t.slice();
next[next.length - 1] = { ...last, reply: last.finalText, done: true };
return next;
Expand Down Expand Up @@ -158,7 +158,7 @@ export function applyStreamEvent(turns, ev, now) {
const turn = turns[i];
let patch;
if (ev.type === 'guarded_text_delta') patch = { reply: turn.reply + (ev.text || '') };
else if (ev.type === 'assistant_text') patch = { finalText: ev.text };
else if (ev.type === 'assistant_text') patch = { finalText: (turn.finalText || '') + ev.text };
else if (ev.type === 'progress') patch = { steps: [...turn.steps, { summary: ev.summary, at: now }] };
else if (ev.type === 'result') {
const base = ev.is_error === true
Expand Down Expand Up @@ -381,11 +381,12 @@ function DraftPrompt({ busy, drafting, onDraft }) {
}

export function Thread({
turns, onChipSend, chipDisabled, threadRef,
turns, onChipSend, chipDisabled, threadRef, bottomRef,
}) {
return (
<div class="rt-thread" ref={threadRef}>
{turns.map((t, i) => <Bubble key={i} turn={t} onChipSend={onChipSend} chipDisabled={chipDisabled} />)}
<div ref={bottomRef} aria-hidden="true" />
</div>
);
}
Expand Down Expand Up @@ -452,6 +453,7 @@ export function ChatPanel({ resetNonce = 0, onTopicPublished }) {
const composerRef = useRef(null);
const threadRef = useRef(null);
const stickToBottomRef = useRef(true);
const bottomRef = useRef(null);
const userInteractedRef = useRef(false);

useEffect(() => {
Expand Down Expand Up @@ -482,7 +484,11 @@ export function ChatPanel({ resetNonce = 0, onTopicPublished }) {
// up to read history.
useEffect(() => {
const el = threadRef.current;
if (el && stickToBottomRef.current) el.scrollTop = el.scrollHeight;
if (!el || !stickToBottomRef.current) return;
el.scrollTop = el.scrollHeight;
// The thread may not be the scrolling ancestor (e.g. the admin page body
// scrolls instead); a sentinel scrollIntoView follows whichever it is.
bottomRef.current?.scrollIntoView({ block: 'nearest' });
}, [turns]);

useEffect(() => {
Expand Down Expand Up @@ -582,7 +588,7 @@ export function ChatPanel({ resetNonce = 0, onTopicPublished }) {
<span class="rt-head-grow" />
<button class="rt-iconbtn" type="button" onClick={newTopic} title="New conversation" aria-label="New conversation">+</button>
</div>
<Thread turns={turns} chipDisabled={busy || draftBusy} onChipSend={(msg) => send(msg)} threadRef={threadRef} />
<Thread turns={turns} chipDisabled={busy || draftBusy} onChipSend={(msg) => send(msg)} threadRef={threadRef} bottomRef={bottomRef} />
{topicDraft && (
<div class="rt-thread-extras">
<OutcomeCard
Expand Down
15 changes: 14 additions & 1 deletion assets/src/publish-dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,27 @@ export function PublishDialog({ draft, busy, onCancel, onPublish }) {
const activeType = TYPES.find((t) => t.id === type) || TYPES[0];

return (
<div class="rt-modal-backdrop rt-pub-backdrop" role="dialog" aria-modal="true" aria-label="Review and publish topic">
<div
class="rt-modal-backdrop rt-pub-backdrop"
role="dialog"
aria-modal="true"
aria-label="Review and publish topic"
onClick={(e) => { if (e.target === e.currentTarget && !busy) onCancel(); }}
>
<div class="rt-modal rt-pub-modal">
<div class="rt-pub-head">
<div>
<h2 class="rt-modal-title">Review &amp; publish</h2>
<p class="rt-modal-sub">Edit the draft below. After you submit, a maintainer reviews it before it appears in the public list.</p>
</div>
<span class={'rt-badge ' + activeType.badgeClass}>{activeType.label}</span>
<button
type="button"
class="rt-modal-close"
aria-label="Close"
disabled={busy}
onClick={onCancel}
>×</button>
</div>
<div class="rt-pub-types" role="group" aria-label="Topic type">
{TYPES.map((t) => (
Expand Down
17 changes: 16 additions & 1 deletion assets/src/styles.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions assets/src/topics.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export function mapCaseToTopic(row) {
const isPending = visibility === 'pending';
return {
id: row.id,
type,
title: row.title || '',
snippet: summary,
typeLabel: TYPE_LABELS[type] || type,
Expand Down
21 changes: 21 additions & 0 deletions assets/test/components.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,27 @@ test('sendTurn falls back to the buffered endpoint when onError fires before any
assert.equal(getBusy(), false);
});

test('sendTurn recovers a tail-truncated reply from finalText when the stream ends (lost trailing deltas)', async () => {
const [getTurns, setTurns] = fakeState([]);
const [getBusy, setBusy] = fakeState(false);

const full = 'Of course! Go ahead — what would you like to know?';
const streamFn = async (text, { onEvent, onDone }) => {
onEvent({ type: 'guarded_text_delta', text: 'Of course! Go ahead — what would y' });
onEvent({ type: 'assistant_text', text: full });
onEvent({ type: 'result' });
onDone();
};

await sendTurn('Can I ask something else?', { setTurns, setBusy, streamFn, sendFn: async () => ({ events: [] }) });

const turns = getTurns();
const last = turns[turns.length - 1];
assert.equal(last.reply, full);
assert.equal(last.done, true);
assert.equal(getBusy(), false);
});

test('sendTurn does not fall back once a stream event already arrived — a later onError just ends the turn in error state', async () => {
const [getTurns, setTurns] = fakeState([]);
const [getBusy, setBusy] = fakeState(false);
Expand Down
Loading