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: 2 additions & 0 deletions src/Schuly.Frontend/public/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,8 @@
"run": {
"pages": "{count, plural, one {# Seite} other {# Seiten}}",
"times": "{count, plural, one {# Mal} other {# Mal}}",
"earlier": "{count, plural, one {+# früherer Schritt} other {+# frühere Schritte}}",
"fewer": "Weniger Schritte zeigen",
"worked": "Erledigt",
"steps": "{count, plural, one {# Schritt} other {# Schritte}}",
"failed": "{count, plural, one {# Schritt hat nicht geklappt} other {# Schritte haben nicht geklappt}}"
Expand Down
2 changes: 2 additions & 0 deletions src/Schuly.Frontend/public/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,8 @@
"run": {
"pages": "{count, plural, one {# page} other {# pages}}",
"times": "{count, plural, one {# time} other {# times}}",
"earlier": "{count, plural, one {+# earlier step} other {+# earlier steps}}",
"fewer": "Show fewer steps",
"worked": "Worked it out",
"steps": "{count, plural, one {# step} other {# steps}}",
"failed": "{count, plural, one {# step didn't work} other {# steps didn't work}}"
Expand Down
2 changes: 2 additions & 0 deletions src/Schuly.Frontend/public/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,8 @@
"run": {
"pages": "{count, plural, one {# page} other {# pages}}",
"times": "{count, plural, one {# fois} other {# fois}}",
"earlier": "{count, plural, one {+# étape précédente} other {+# étapes précédentes}}",
"fewer": "Afficher moins d'étapes",
"worked": "C'est fait",
"steps": "{count, plural, one {# étape} other {# étapes}}",
"failed": "{count, plural, one {# étape n'a pas marché} other {# étapes n'ont pas marché}}"
Expand Down
2 changes: 2 additions & 0 deletions src/Schuly.Frontend/public/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,8 @@
"run": {
"pages": "{count, plural, one {# pagina} other {# pagine}}",
"times": "{count, plural, one {# volta} other {# volte}}",
"earlier": "{count, plural, one {+# passaggio precedente} other {+# passaggi precedenti}}",
"fewer": "Mostra meno passaggi",
"worked": "Fatto",
"steps": "{count, plural, one {# passaggio} other {# passaggi}}",
"failed": "{count, plural, one {# passaggio non ha funzionato} other {# passaggi non hanno funzionato}}"
Expand Down
2 changes: 2 additions & 0 deletions src/Schuly.Frontend/public/i18n/rm.json
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,8 @@
"run": {
"pages": "{count, plural, one {# pagina} other {# paginas}}",
"times": "{count, plural, one {# giada} other {# giadas}}",
"earlier": "{count, plural, one {+# pass pli baud} other {+# pass pli baud}}",
"fewer": "Mussar main pass",
"worked": "Fatg",
"steps": "{count, plural, one {# pass} other {# pass}}",
"failed": "{count, plural, one {# pass n'ha betg funcziunà} other {# pass n'han betg funcziunà}}"
Expand Down
25 changes: 24 additions & 1 deletion src/Schuly.Frontend/src/app/pages/helply/helply-timeline.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { HelplySettings, ToolActivity } from '../../core/helply-types';
import { buildTimeline, chainSummary, runDetail, stepIcon } from './helply-timeline';
import { buildTimeline, chainBlocks, chainSummary, runDetail, runSummary, stepIcon } from './helply-timeline';
import { starterPrompts } from './helply-suggestions';

const tool = (id: string, at: number | null, change: Partial<ToolActivity> = {}): ToolActivity => ({
Expand Down Expand Up @@ -67,6 +67,29 @@ describe('buildTimeline', () => {
});
});

describe('chainBlocks and runSummary', () => {
it('groups consecutive steps into runs between the text', () => {
const text = 'First.\n\nSecond.\n\nAnswer.';
const first = text.indexOf('First.') + 'First.'.length;
const second = text.indexOf('Second.') + 'Second.'.length;
const timeline = buildTimeline(text, [tool('a', first), tool('b', first, { label: 'Read an email' }), tool('c', second)]);
const blocks = chainBlocks(timeline.chain);
expect(blocks.map((b) => (b.kind === 'text' ? b.text : b.steps.map((s) => s.label)))).toEqual([
'First.',
['Read your mail list', 'Read an email'],
'Second.',
['Read your mail list'],
]);
});

it('sums folded runs in the one-line summary', () => {
const timeline = buildTimeline('', [tool('a', 0), tool('b', 0), tool('c', 0, { label: 'Read an email' })]);
const steps = chainBlocks(timeline.chain).flatMap((b) => (b.kind === 'steps' ? b.steps : []));
expect(runSummary(steps)).toBe('Read your mail list · Read an email · 3 steps');
expect(runSummary([])).toBe('Worked it out');
});
});

describe('runDetail and stepIcon', () => {
it('says how a run of steps went', () => {
expect(runDetail([null])).toBeNull();
Expand Down
29 changes: 25 additions & 4 deletions src/Schuly.Frontend/src/app/pages/helply/helply-timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,31 @@ export function buildTimeline(text: string, tools: readonly ToolActivity[]): Rep
return { chain, answer: text.slice(position).trim(), steps: tools.length };
}

/** A finished chain in one line: "Read your mail list · 4 steps", or the two kinds of step it took. */
export function chainSummary(timeline: ReplyTimeline): string {
const labels = [...new Set(timeline.chain.flatMap((e) => (e.kind === 'step' ? [e.label] : [])))];
/** A stretch of the chain: text Helply wrote, or the run of steps it took between two stretches of text. */
export type ChainBlock = { kind: 'text'; key: string; text: string } | { kind: 'steps'; key: string; steps: ChainStep[] };

/** The chain with its consecutive steps grouped, so each run folds and opens on its own. */
export function chainBlocks(chain: readonly ChainEntry[]): ChainBlock[] {
const blocks: ChainBlock[] = [];
for (const entry of chain) {
const last = blocks[blocks.length - 1];
if (entry.kind === 'text') blocks.push({ kind: 'text', key: entry.key, text: entry.text });
else if (last?.kind === 'steps') last.steps.push(entry);
else blocks.push({ kind: 'steps', key: `steps-${entry.key}`, steps: [entry] });
}
return blocks;
}

/** A finished run in one line: "Read your mail list · 4 steps", or the two kinds of step it took. */
export function runSummary(steps: readonly ChainStep[]): string {
const labels = [...new Set(steps.map((s) => s.label))];
if (!labels.length) return t('helply.run.worked');
const shown = labels.slice(0, 2).join(' · ');
return timeline.steps > 1 ? `${shown} · ${t('helply.run.steps', { count: timeline.steps })}` : shown;
const total = steps.reduce((sum, s) => sum + s.count, 0);
return total > 1 ? `${shown} · ${t('helply.run.steps', { count: total })}` : shown;
}

/** A finished chain in one line. */
export function chainSummary(timeline: ReplyTimeline): string {
return runSummary(timeline.chain.filter((e): e is ChainStep => e.kind === 'step'));
}
16 changes: 16 additions & 0 deletions src/Schuly.Frontend/src/app/pages/helply/message.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,20 @@ describe('Helply message markdown', () => {
expect(link.getAttribute('rel')).toContain('noopener');
expect(el.querySelector('pk-markdown')!.textContent).toContain('click here (evil.example)');
});

it('shows the text between steps as part of the reply, with each run of steps between', async () => {
const fixture = TestBed.createComponent(HelplyMessageView);
const text = 'Checking mail.\n\nAnswer.';
const at = 'Checking mail.'.length;
fixture.componentRef.setInput('message', {
...message(text),
tools: [{ id: 't1', label: 'Read your mail list', status: 'done', at }],
});
fixture.detectChanges();
await fixture.whenStable();
const el = fixture.nativeElement as HTMLElement;
expect(el.querySelectorAll('app-reply-chain')).toHaveLength(1);
expect(el.querySelectorAll('pk-markdown')).toHaveLength(2);
expect(el.textContent).toContain('Checking mail.');
});
});
22 changes: 14 additions & 8 deletions src/Schuly.Frontend/src/app/pages/helply/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { HlmButton } from '@spartan-ng/helm/button';
import { ChatMessage } from '../../core/helply-state';
import { HelplyAttachment } from '../../core/helply-types';
import { AttachmentCards } from './attachment-cards';
import { buildTimeline } from './helply-timeline';
import { buildTimeline, chainBlocks } from './helply-timeline';
import { ReplyChain } from './reply-chain';

/**
Expand All @@ -18,11 +18,11 @@ import { ReplyChain } from './reply-chain';
* highlighting, is styled here.
*/
const MARKDOWN =
'text-sm leading-relaxed break-words ' +
'break-words ' +
'[&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 ' +
'[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:my-0.5 ' +
'[&_h1]:mt-3 [&_h1]:mb-1 [&_h1]:text-base [&_h1]:font-semibold [&_h2]:mt-3 [&_h2]:mb-1 [&_h2]:text-base [&_h2]:font-semibold ' +
'[&_h3]:mt-2 [&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-semibold ' +
'[&_h1]:mt-4 [&_h1]:mb-1 [&_h1]:text-xl [&_h1]:font-semibold [&_h2]:mt-4 [&_h2]:mb-1 [&_h2]:text-lg [&_h2]:font-semibold ' +
'[&_h3]:mt-3 [&_h3]:mb-1 [&_h3]:text-base [&_h3]:font-semibold [&_:is(h1,h2,h3):first-child]:mt-0 ' +
'[&_a]:text-primary [&_a]:underline [&_a+span]:text-muted-foreground [&_a]:underline-offset-2 [&_a]:rounded-sm [&_a:focus-visible]:ring-ring/50 [&_a:focus-visible]:ring-3 [&_a]:outline-none ' +
'[&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:rounded [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_code]:font-mono [&_:not(pre)>code]:text-xs ' +
'[&_.pk-code-container>pre]:bg-muted [&_.pk-code-container>pre]:my-3 [&_.pk-code-container>pre]:overflow-x-auto [&_.pk-code-container>pre]:rounded-md [&_.pk-code-container>pre]:border [&_.pk-code-container>pre]:p-3 [&_.pk-code-container>pre]:text-[13px] ' +
Expand All @@ -47,10 +47,15 @@ const MARKDOWN =
} @else {
<article aria-label="Helply" class="flex w-full min-w-0 gap-3">
<img src="helply-avatar.svg" alt="" width="32" height="32" class="mt-0.5 size-8 shrink-0" />
<pk-assistant-turn class="min-w-0 flex-1 gap-2" [copyText]="copyText()" [streaming]="message().streaming">
<div class="flex min-w-0 flex-col gap-3">
@if (timeline().chain.length) {
<app-reply-chain [timeline]="timeline()" [streaming]="message().streaming" />
<pk-assistant-turn class="min-w-0 flex-1" [copyText]="copyText()" [streaming]="message().streaming">
<div class="flex min-w-0 flex-col gap-4">
<!-- Text and runs of steps in the order they happened; a run is open while it is being taken. -->
@for (block of blocks(); track block.key; let last = $last) {
@if (block.kind === 'steps') {
<app-reply-chain [steps]="block.steps" [active]="message().streaming && last && !timeline().answer" />
} @else {
<pk-markdown externalImages="none" rawHtml="escape" linkPolicy="strict" [class]="markdownClass" [content]="block.text" />
}
}
@if (timeline().answer) {
<pk-markdown externalImages="none" rawHtml="escape" linkPolicy="strict" [class]="markdownClass" [content]="timeline().answer" />
Expand Down Expand Up @@ -95,6 +100,7 @@ export class HelplyMessageView {
protected readonly isUser = computed(() => this.message().role === 'user');
/** The steps Helply took, with the text it wrote between them, and its answer after the last one. */
protected readonly timeline = computed(() => buildTimeline(this.message().text, this.message().tools));
protected readonly blocks = computed(() => chainBlocks(this.timeline().chain));
/** Copy takes the answer; a reply that is all steps copies its whole text. */
protected readonly copyText = computed(() => this.timeline().answer || this.message().text);
/** Waiting for the model: nothing written yet after the last step and no step running. */
Expand Down
Loading
Loading