Make the Instruction agent reply in the user's language, at a human pace
The agent answers in the language each message is written in (Indonesian or English), whatever the UI language; short replies such as "LLM" keep the conversation's language. inLang() runs t() in that language. Each turn now reads like a person: a thinking step with what the agent is doing, the reply typed out with "Thought for Ns" above it, a short typing pause, then one question, with its options shown once it is typed. Reduced motion skips the animation. "dari file LLM" / "from the document X" is read as the topic, and topics keep the user's capitals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { EMPTY, nextQuestion, respond, type AgentState } from './agent'
|
||||
import { chatLangOf, EMPTY, nextQuestion, respond, type AgentState } from './agent'
|
||||
import { t } from '../i18n'
|
||||
|
||||
const say = (...inputs: string[]) => inputs.reduce<AgentState>((s, input) => respond(s, input).state, EMPTY)
|
||||
@@ -71,4 +71,16 @@ describe('instruction agent', () => {
|
||||
expect(nextQuestion(s)?.type).toBe('tooMany')
|
||||
expect(rules(say('10 summaries per day about sales', '30 per week'))[0]).toMatchObject({ count: 30, per: 'week' })
|
||||
})
|
||||
|
||||
it('takes "dari file X" as the topic', () => {
|
||||
expect(rules(say('Saya mau buat video dari file LLM'))[0]).toMatchObject({ kind: 'video', topic: 'LLM' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('chatLangOf', () => {
|
||||
it('tells Indonesian from English, and stays undecided on short replies', () => {
|
||||
expect(chatLangOf('Saya mau buat video dari file LLM')).toBe('id')
|
||||
expect(chatLangOf('I want to make 2 quizzes per week')).toBe('en')
|
||||
expect(chatLangOf('LLM')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// draft plan, and the agent asks about one gap at a time (a part it did not understand, a video
|
||||
// cadence, a weekday, a topic, the output language) right in the chat, so nothing is left for the
|
||||
// user to spot alone.
|
||||
import { t, type Key } from '../i18n'
|
||||
import { t, type Key, type Lang } from '../i18n'
|
||||
import {
|
||||
dayName, describeRule, LANGUAGES, languageLabel, languageOf, parseInstruction, videoCadence, weekdayOf, weeklyTarget,
|
||||
type ContentRule, type OutputLanguage, type ResearchRule, type Rule,
|
||||
@@ -81,6 +81,22 @@ export function editRule(s: AgentState, id: number, patch: Partial<ContentRule>
|
||||
|
||||
export const removeRule = (s: AgentState, id: number): AgentState => ({ ...s, items: s.items.filter(i => i.id !== id) })
|
||||
|
||||
// Words that tell which language a message is written in, so the agent can answer in it.
|
||||
const ID_CHAT = new Set(('saya aku mau ingin bikin bikinin buat buatkan tolong dong ya iya tidak gak nggak enggak yang dan dengan untuk dari '
|
||||
+ 'ke di ini itu apa bagaimana gimana hari minggu bulan setiap tiap soal tentang mengenai riset ringkasan kuis lewati hapus seluruh '
|
||||
+ 'otak senin selasa rabu kamis jumat sabtu sip lanjut bahasa pakai jadi kalau kalo juga aja saja sekarang mulai').split(' '))
|
||||
const EN_CHAT = new Set(('i me my want would like make create please the and with for from to in this that what how day week month every '
|
||||
+ 'each about research summary summaries quiz quizzes skip remove whole brain monday tuesday wednesday thursday friday saturday '
|
||||
+ 'sunday yes no okay sure start use language also just now').split(' '))
|
||||
|
||||
/** The language a message is written in, or null when it is too short or mixed to tell. */
|
||||
export function chatLangOf(text: string): Lang | null {
|
||||
const words = text.toLowerCase().match(/\p{L}+/gu) ?? []
|
||||
const id = words.filter(w => ID_CHAT.has(w)).length
|
||||
const en = words.filter(w => EN_CHAT.has(w)).length
|
||||
return id > en ? 'id' : en > id ? 'en' : null
|
||||
}
|
||||
|
||||
export interface Turn { state: AgentState; reply: string; approve?: boolean }
|
||||
|
||||
const YES = /^(ya|iya|yes|y|ok|oke|okay|okey|setuju|approve|lanjut|gas|sip|go|boleh)\b/
|
||||
|
||||
@@ -97,7 +97,8 @@ export interface ParseResult {
|
||||
language?: OutputLanguage
|
||||
}
|
||||
|
||||
const TOPIC = /\b(?:soal|tentang|mengenai|seputar|terkait|topik|bertema|perihal|about|regarding|covering|related to|on the topic of|topic)\s+(?!latihan\b)(.+)$/
|
||||
// "dari file LLM" / "from the document onboarding" names a Brain file, which works as the topic.
|
||||
const TOPIC = /\b(?:soal|tentang|mengenai|seputar|terkait|topik|bertema|perihal|about|regarding|covering|related to|on the topic of|topic|(?:dari|from)(?: the)? (?:file|berkas|dokumen|documents?|materi|material))\s+(?!latihan\b)(.+)$/
|
||||
const FREQUENCY_TAIL = /\b(tiap|setiap|per|sehari|seminggu|every|each|daily|weekly|monthly|a day|a week|a month)\b.*$/
|
||||
|
||||
function topicOf(clause: string): string {
|
||||
@@ -176,6 +177,14 @@ export function parseInstruction(text: string): ParseResult {
|
||||
const topics = new Set(rules.map(r => r.topic).filter(Boolean))
|
||||
const fallback = shared || (topics.size === 1 ? [...topics][0] : '')
|
||||
for (const r of rules) if (!r.topic) r.topic = fallback
|
||||
// Topics are found in lower case; they keep the user's own capitals ("LLM", "HR").
|
||||
const lowered = text.toLowerCase()
|
||||
if (lowered.length === text.length) {
|
||||
for (const r of rules) {
|
||||
const at = r.topic ? lowered.indexOf(r.topic) : -1
|
||||
if (at >= 0) r.topic = text.slice(at, at + r.topic.length)
|
||||
}
|
||||
}
|
||||
return language ? { rules, unknown, language } : { rules, unknown }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user