From c8b9c7502079632859b12c684c487ae1d92c0b2b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 03:16:10 +0000 Subject: [PATCH] A short welcome on the first visit, and What's new after an update The first time someone opens Shell:B, a three-screen welcome (always skippable) says what it is and how agents work; its last button leads to the new chat page's own What Shell:B can do section. After an update, the new chat page shows a small What's new card once, built from an optional `news` line on docs/changelog.json entries. Both are remembered on the server (kv "onboarding"), so they don't come back on another device; a guest's cell keeps its own. People who already have chats skip the welcome and see the latest news instead. French for every new string, appended to fr.json. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018go55qk4ihWMhkzn8r7rpK --- AGENTS.md | 2 +- CONTRIBUTING.md | 4 +- docs/changelog.json | 10 ++++ server/app.py | 2 + server/changelog.py | 8 ++- server/onboarding.py | 56 ++++++++++++++++++ tests/changelog_test.py | 10 +++- tests/onboarding_test.py | 54 +++++++++++++++++ ui/src/App.tsx | 3 +- ui/src/api.ts | 4 +- ui/src/components/NewChatHome.tsx | 17 +++++- ui/src/components/Onboarding.tsx | 96 +++++++++++++++++++++++++++++++ ui/src/i18n/fr.json | 16 +++++- ui/src/mobile/MobileApp.tsx | 4 ++ ui/src/types.ts | 4 +- 15 files changed, 278 insertions(+), 12 deletions(-) create mode 100644 server/onboarding.py create mode 100644 tests/onboarding_test.py create mode 100644 ui/src/components/Onboarding.tsx diff --git a/AGENTS.md b/AGENTS.md index b6d9ef7..f10c993 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ This directory (`~/shellb/web`) houses the web frontend, agent runtime server, d - No service restart required for static UI changes, but reload browser. 3. **Rules for Agents**: - - **Shipping a user-visible feature? Add an entry (newest first) to `docs/changelog.json`** — it feeds Settings → Changelog. + - **Shipping a user-visible feature? Add an entry (newest first) to `docs/changelog.json`** — it feeds Settings → Changelog. If everyone (a light user included) would notice and care, add a `news` line too: one plain sentence, shown once on the What's new card, with its French in `ui/src/i18n/fr.json`. - **Do not create Node.js backend files.** - **Do not create untyped `.jsx` files.** - **Files written to `/work` in sandbox do not affect the web application.** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3666d02..2093ea6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,7 +79,9 @@ to humans too. regression suite in `tests/` and type-checks the UI. CI runs the same compile, lint and type check on every pull request. A pull request that fails them won't be reviewed until it passes. 2. **Shipping something a user will notice?** Add an entry at the top of `docs/changelog.json` (`date`, `area`, - `title`, `summary`). It feeds Settings → Changelog in the app and the notes of the next release. + `title`, `summary`). It feeds Settings → Changelog in the app and the notes of the next release. A change everyone + would notice can also have `news`: one plain sentence for the What's new card on the new chat page, shown once to + each person (add its French too; `tests/changelog_test.py` checks). 3. **New UI text?** Wrap it in `t('…')` and add the French line to `ui/src/i18n/fr.json`. `tests/i18n_test.py` fails on any English string without a translation, and it tells you which. 4. **Touching a route, a tool or a table?** Update the matching section of `docs/MANUAL.md` (or `docs/API.md` for diff --git a/docs/changelog.json b/docs/changelog.json index 4f0bfe0..f0e3dcf 100644 --- a/docs/changelog.json +++ b/docs/changelog.json @@ -1,4 +1,10 @@ [ + { + "date": "2026-10-13", + "area": "Chat", + "title": "A short welcome on your first visit, and What's new after an update", + "summary": "The first time someone opens Shell:B, a short welcome (three screens, skippable) says what it is, how to pick an agent and where to start; its last button leads to What Shell:B can do on the new chat page. After an update, the new chat page shows a small What's new card once, with up to three plain-language lines; See all changes opens Settings → Changelog. Both are remembered on the server for each person, so they don't come back on another device. Changelog entries reach the card through an optional news line in docs/changelog.json." + }, { "date": "2026-10-13", "area": "Kiosk", @@ -9,6 +15,7 @@ "date": "2026-10-13", "area": "Agents", "title": "Agent Store: install ready-made agents in one click", + "news": "The Agent Lab has a Store of ready-made agents, such as an editor, a tutor and a trip planner, to install in one click.", "summary": "The Agent Lab has a Store with ready-made agents from the Shell:B Agents collection on GitHub: Scribe for editing, Tutor for studying, Quartermaster for the household, Navigator for trips, Gamemaster for tabletop games and Chronicler for journaling. Search them or filter by topic, read what each one can use, and press Install: the agent joins your agent menu and runs on your own model, never a cloud one. More agents in the agent menu, and the phone's agent sheet, have a Browse the Agent Store link. Agent repositories added in Settings → Community show up in the Store too." }, { @@ -21,6 +28,7 @@ "date": "2026-10-13", "area": "Chat", "title": "The new chat page shows what Shell:B can do", + "news": "A new chat now shows what Shell:B can do: everyday tasks to try, and what each agent and app is for.", "summary": "Below the agent's starter cards, a new chat now has a What Shell:B can do section with three tabs. Try something offers everyday tasks (prepare for a job interview, polish a cover letter, identify a sea creature, step back into the Neolithic, get notes on your writing, research a topic, summarise a document, make a picture, get a quick answer); each one switches the chat to the agent best at it and sends. Agents lists every agent with what it is for: picking one sets it for the chat and its starter cards change to match. Apps lists every app with a line on what it does, and opens it." }, { @@ -57,6 +65,7 @@ "date": "2026-10-11", "area": "Agents", "title": "Thalassa, a marine biologist; species lists, survey diversity, literature and photo-ID checks", + "news": "Thalassa, a marine biologist, can help you identify sea creatures and find the research on them.", "summary": "New built-in agent Thalassa (🐙) for marine science. The marine tool gains: match, which checks a species list or survey sheet (CSV, TSV, TXT or Excel, French headers too) against WoRMS for misspellings, outdated names and homonyms and saves the corrected table; diversity, with richness, Shannon, Pielou, Simpson and Chao1 per site and pooled; literature, which finds papers through OpenAlex with authors, journal, citations, DOI and open-access links; and verify, which checks whether candidate species for a photo or sighting have been recorded near where it was taken. Species cards now list the Habitats Directive, CITES, IUCN Red List and other policy lists. Type /species in the composer to use it from any chat." }, { @@ -69,6 +78,7 @@ "date": "2026-10-11", "area": "Languages", "title": "The whole interface in French", + "news": "Shell:B can speak French: choose the language in Settings → Languages.", "summary": "Research, Agent Lab, Tools & Connectors (API keys, guests, connectors, discover, skills, screen, webhooks), Models and the Model Store, Fleet, Wiki, Radio and the DJ, Shopping, Circles, Secretary, Scheduled, Mega plans, Assets, Generate, Notes, Me, Offline data, the kiosk and the smaller cards follow the interface language. About 1,800 more strings; the catalog holds 3,800." }, { diff --git a/server/app.py b/server/app.py index 2ce18d8..cf5a48d 100644 --- a/server/app.py +++ b/server/app.py @@ -103,6 +103,7 @@ import sysinfo import network import changelog +import onboarding import memory import episodes import chatmail @@ -246,6 +247,7 @@ async def __call__(self, scope, receive, send): app.include_router(sysinfo.router) app.include_router(network.router) app.include_router(changelog.router) +app.include_router(onboarding.router) app.include_router(asset_libs.router) app.include_router(groups.router) app.include_router(docview.router) diff --git a/server/changelog.py b/server/changelog.py index 0354f21..5158625 100644 --- a/server/changelog.py +++ b/server/changelog.py @@ -1,9 +1,13 @@ """Feature changelog (Settings → Changelog). The source of truth is docs/changelog.json, a hand-kept list (newest first) of -{date: YYYY-MM-DD, area, title, summary}. It is versioned with the web tree, so agents and the owner add an entry +{date: YYYY-MM-DD, area, title, summary, news?}. It is versioned with the web tree, so agents and the owner add an entry in the same change that ships a feature. This module only serves it, so a bad edit shows up as an empty list with an error note rather than a 500. + +`news` is optional: one short, plain sentence for everyone, shown once on the What's new card of the new chat page +(onboarding.py). Give it only to changes a light user would notice and care about, and add its French to +ui/src/i18n/fr.json (tests/changelog_test.py checks). """ import json from pathlib import Path @@ -24,7 +28,7 @@ def load() -> tuple[list[dict], str]: out = [] for e in raw: if isinstance(e, dict) and e.get("title") and e.get("date"): - out.append({k: str(e.get(k, "")) for k in ("date", "area", "title", "summary")}) + out.append({k: str(e.get(k, "")) for k in ("date", "area", "title", "summary", "news")}) out.sort(key=lambda e: e["date"], reverse=True) # stable, so same-day entries keep file order return out, "" diff --git a/server/onboarding.py b/server/onboarding.py new file mode 100644 index 0000000..6e04297 --- /dev/null +++ b/server/onboarding.py @@ -0,0 +1,56 @@ +"""First-visit welcome and the "What's new" card on the new chat page. + +Remembered on the server (kv "onboarding") rather than per browser, so a person who has seen the welcome on their laptop +doesn't get it again on their phone. A guest's cell has its own database, so each guest is remembered separately. + +What's new is built from docs/changelog.json: an entry with a `news` line (one plain sentence for everyone) shows on +the card once. Entries without one are for Settings → Changelog only. The first time this runs for someone: +- nothing in the database yet → a new person: show the welcome, and count today's news as already seen; +- conversations already there → someone using Shell:B before this shipped: no welcome, show the latest news. +""" +from fastapi import APIRouter, Request + +import changelog +import db + +router = APIRouter(prefix="/api/onboarding") +KEY = "onboarding" +SHOWN = 3 # news lines on the card at most; older unseen ones are marked seen with them + + +def _news() -> list[dict]: + entries, _ = changelog.load() + return [e for e in entries if e.get("news")] + + +def _state() -> dict: + st = db.kv_get(KEY) + if isinstance(st, dict): + return st + with db.conn() as c: + used = c.execute("SELECT 1 FROM conversations LIMIT 1").fetchone() is not None + st = {"welcomed": used, "seen": [] if used else [e["title"] for e in _news()]} + db.kv_set(KEY, st) + return st + + +@router.get("") +def get(): + st = _state() + seen = set(st.get("seen", [])) + fresh = [] if not st.get("welcomed") else [e for e in _news() if e["title"] not in seen] + return {"welcome": not st.get("welcomed"), + "news": [{"date": e["date"], "title": e["title"], "news": e["news"]} for e in fresh[:SHOWN]]} + + +@router.post("/seen") +async def seen(req: Request): + """{"what": "welcome"} after the welcome is finished or skipped; {"what": "news"} once the card has been shown. + Either way every current news line counts as seen: a new person shouldn't get a What's new right after the welcome.""" + body = await req.json() + st = _state() + if body.get("what") == "welcome": + st["welcomed"] = True + st["seen"] = sorted(set(st.get("seen", [])) | {e["title"] for e in _news()}) + db.kv_set(KEY, st) + return {"ok": True} diff --git a/tests/changelog_test.py b/tests/changelog_test.py index d2714c2..6efff42 100644 --- a/tests/changelog_test.py +++ b/tests/changelog_test.py @@ -1,4 +1,6 @@ -"""The shipped docs/changelog.json parses, is well-formed, and /api/changelog serves it newest first.""" +"""The shipped docs/changelog.json parses, is well-formed, and /api/changelog serves it newest first. Every `news` line +(the What's new card) has a French entry, since the UI translates it from a variable the i18n scan can't see.""" +import json import unittest from pathlib import Path @@ -18,6 +20,12 @@ def test_file_is_well_formed(self): self.assertRegex(e["date"], r"^\d{4}-\d\d-\d\d$") self.assertTrue(e["title"]) + def test_news_lines_are_translated(self): + entries, _ = changelog.load() + fr = json.loads((changelog.FILE.parent.parent / "ui" / "src" / "i18n" / "fr.json").read_text(encoding="utf-8")) + missing = [e["news"] for e in entries if e["news"] and e["news"] not in fr] + self.assertEqual(missing, []) + def test_endpoint_newest_first(self): r = client.get("/api/changelog").json() dates = [e["date"] for e in r["entries"]] diff --git a/tests/onboarding_test.py b/tests/onboarding_test.py new file mode 100644 index 0000000..dfcc33f --- /dev/null +++ b/tests/onboarding_test.py @@ -0,0 +1,54 @@ +"""The first-visit welcome and the What's new card (server/onboarding.py): who gets the welcome, news shown once, and +the welcome counting as having seen today's news.""" +import unittest +from pathlib import Path + +from smoke_test import client, shellb # noqa: F401 — sets up the scratch copy + +import changelog +import db + +changelog.FILE = Path(__file__).resolve().parent.parent / "docs" / "changelog.json" # the scratch copy has no docs/ + + +def _forget(): + with db.conn() as c: + c.execute("DELETE FROM kv WHERE key='onboarding'") + + +def _has_chats(): + with db.conn() as c: + return c.execute("SELECT 1 FROM conversations LIMIT 1").fetchone() is not None + + +class Onboarding(unittest.TestCase): + def tearDown(self): + _forget() + + def test_first_run_depends_on_existing_chats(self): + _forget() + r = client.get("/api/onboarding").json() + if _has_chats(): # used before this shipped: no welcome, the latest news instead + self.assertFalse(r["welcome"]) + self.assertTrue(r["news"]) + else: # a new person: the welcome, and nothing as "new" + self.assertTrue(r["welcome"]) + self.assertEqual(r["news"], []) + + def test_news_shows_once(self): + db.kv_set("onboarding", {"welcomed": True, "seen": []}) + news = client.get("/api/onboarding").json()["news"] + self.assertTrue(0 < len(news) <= 3) + self.assertTrue(all(n["news"] for n in news)) + client.post("/api/onboarding/seen", json={"what": "news"}) + self.assertEqual(client.get("/api/onboarding").json()["news"], []) + + def test_welcome_done_hides_it_and_current_news(self): + db.kv_set("onboarding", {"welcomed": False, "seen": []}) + self.assertTrue(client.get("/api/onboarding").json()["welcome"]) + client.post("/api/onboarding/seen", json={"what": "welcome"}) + self.assertEqual(client.get("/api/onboarding").json(), {"welcome": False, "news": []}) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 6ffca89..077d899 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -902,7 +902,8 @@ export function App() { onScroll={e => { const el = e.currentTarget; setAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 80); }}> {messages.length === 0 ? ( send(prompt, [])} onStart={startWith} onSelectAgent={selectAgent} onOpenApp={openApp} /> + onSend={prompt => send(prompt, [])} onStart={startWith} onSelectAgent={selectAgent} onOpenApp={openApp} + onOpenChangelog={() => { setSettingsTab('changelog'); setModal('settings'); }} /> ) : (
{foldSummarized(messages).map(m => ( diff --git a/ui/src/api.ts b/ui/src/api.ts index 48f766a..5ccef29 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -6,7 +6,7 @@ import type { ScheduledTask, ScheduledRun, ScheduleDraft, ResearchBoard, ResearchBoardDetail, ResearchItem, NotebookMessage, NotebookCitation, DiscoverHit, StudioType, MegaPlan, MegaPlanDetail, ProjectTodo, ProjectTodoList, SshHost, ActivityItem, ActivitySummary, AskAnswer, PendingApproval, CodexCollection, CodexEntry, CodexIndex, CliAgent, TtsVoice, Meeting, MeetingSpeaker, MeetingUtterance, MeetingSummaryData, MeetingSummaryItem, - BackupInfo, ChangelogEntry, BackupCreateRequest, BackupRestoreResult, + BackupInfo, ChangelogEntry, Onboarding, BackupCreateRequest, BackupRestoreResult, ChatHit, UsageReport } from './types'; /** The admin key (data/admin.key on the server) unlocks MCP commands, API keys and cloud settings. */ @@ -112,6 +112,8 @@ export const api = { changePassword: (current: string, password: string) => req('/api/auth/password', { method: 'POST', body: json({ current, password }) }), authRateLimitStatus: () => req<{ ip: string; recentIpFailures: number; maxIpFailuresBeforeLock: number; recentGlobalFailures: number; globalLockActive: boolean }>('/api/auth/status'), changelog: () => req<{ entries: ChangelogEntry[]; error: string }>('/api/changelog'), + onboarding: () => req('/api/onboarding'), + onboardingSeen: (what: 'welcome' | 'news') => req('/api/onboarding/seen', { method: 'POST', body: json({ what }) }), backups: () => req('/api/backup'), createBackup: (b?: BackupCreateRequest) => req('/api/backup', { method: 'POST', body: json(b ?? {}) }), restoreBackup: (id: string) => req(`/api/backup/${encodeURIComponent(id)}/restore`, { method: 'POST' }), diff --git a/ui/src/components/NewChatHome.tsx b/ui/src/components/NewChatHome.tsx index 8880a93..6a2483c 100644 --- a/ui/src/components/NewChatHome.tsx +++ b/ui/src/components/NewChatHome.tsx @@ -5,11 +5,13 @@ import type { Agent } from '../types'; import type { Suggestion } from '../suggestions'; import { APPS, GROUP_LABEL, type AppGroup, type AppId } from './apps'; import { AgentAvatar } from './common'; +import { useOnboarding, Welcome, WhatsNew } from './Onboarding'; /** * The empty chat: the agent's own starter cards, then "What Shell:B can do" for people who don't know yet. Three tabs: * Try something (everyday tasks, each answered by the agent best at it), Agents (who is who) and Apps (everything on the - * rail and in All apps). Everyone sees the same page. + * rail and in All apps). Everyone sees the same page, with the first-visit welcome and the What's new card on top of it + * (Onboarding.tsx). */ interface Starter { title: string; blurb: string; prompt: string; agent: string } @@ -47,7 +49,7 @@ type Tab = 'try' | 'agents' | 'apps'; const card = 'sf-card rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-hover)] hover:border-[var(--border-strong)] transition group text-left'; -export function NewChatHome({ agent, agents, quick, greeting, onSend, onStart, onSelectAgent, onOpenApp }: { +export function NewChatHome({ agent, agents, quick, greeting, onSend, onStart, onSelectAgent, onOpenApp, onOpenChangelog }: { agent: Agent; agents: Agent[]; quick: Suggestion[]; @@ -58,9 +60,13 @@ export function NewChatHome({ agent, agents, quick, greeting, onSend, onStart, o onStart: (agent: Agent, prompt: string) => void; onSelectAgent: (agent: Agent) => void; onOpenApp: (app: AppId) => void; + /** Settings → Changelog, from the What's new card */ + onOpenChangelog: () => void; }) { const [tab, setTab] = useState('try'); const top = useRef(null); + const canDo = useRef(null); + const onboarding = useOnboarding(); const showcased = agents.filter(a => !a.unavailable && !NOT_SHOWCASED.has(a.id)); const byId = new Map(showcased.map(a => [a.id, a])); @@ -94,6 +100,7 @@ export function NewChatHome({ agent, agents, quick, greeting, onSend, onStart, o

{agent.name} · {agent.description}

+ {onboarding.news.length > 0 && }
{quick.map(q => (
-
+

{t('What Shell:B can do')}

@@ -176,6 +183,10 @@ export function NewChatHome({ agent, agents, quick, greeting, onSend, onStart, o )}
+ {onboarding.welcome && ( + { setTab('try'); canDo.current?.scrollIntoView({ block: 'start', behavior: 'smooth' }); }} /> + )} +

{t('Overthink. Overcomplicate. Overengineer.')}

); diff --git a/ui/src/components/Onboarding.tsx b/ui/src/components/Onboarding.tsx new file mode 100644 index 0000000..7b48cb2 --- /dev/null +++ b/ui/src/components/Onboarding.tsx @@ -0,0 +1,96 @@ +import { useEffect, useState } from 'react'; +import clsx from 'clsx'; +import { Sparkles, X } from 'lucide-react'; +import { api } from '../api'; +import type { Onboarding } from '../types'; +import { t } from '../i18n'; +import { btn } from './common'; + +/** + * The first-visit welcome and the What's new card, both shown on the new chat page (server/onboarding.py remembers + * them per person, across devices). The welcome is three short screens and always skippable; its last button leads to + * the page's own "What Shell:B can do" section rather than repeating it. What's new lists the `news` lines of + * docs/changelog.json the person hasn't seen, once. Everyone gets the same two things. + */ + +let pending: Promise | undefined; // one request per page load; the new chat page mounts again for every chat + +export function useOnboarding() { + const [state, setState] = useState(); + useEffect(() => { + let live = true; + pending ??= api.onboarding().catch(() => ({ welcome: false, news: [] })); + pending.then(s => { if (live) setState(s); }); + return () => { live = false; }; + }, []); + const update = (s: Onboarding) => { pending = Promise.resolve(s); setState(s); }; + return { + welcome: !!state?.welcome, + news: state?.welcome ? [] : state?.news ?? [], + /** finished or skipped: the welcome counts as the introduction, so nothing older shows as new afterwards */ + doneWelcome: () => { api.onboardingSeen('welcome').catch(() => {}); update({ welcome: false, news: [] }); }, + closeNews: () => update({ welcome: false, news: [] }), + }; +} + +// a function so the i18n scan sees each string and the language can change between visits +const steps = () => [ + { title: t('Welcome to Shell:B'), + body: t('Shell:B is an AI assistant that runs on its own computer, not in a company’s cloud. Type a question or a task in the box at the bottom, and send it.') }, + { title: t('Agents for different jobs'), + body: t('Each agent is good at something different: writing, research, the sea, the distant past, pictures. Pick one in the agent menu by the message box, or type @ and a name.') }, + { title: t('Start with an example'), + body: t('Below each new chat, What Shell:B can do lists everyday things to try, every agent and every app. Pick one and Shell:B takes it from there.') }, +]; + +export function Welcome({ onDone, onShowMe }: { onDone: () => void; onShowMe: () => void }) { + const [step, setStep] = useState(0); + const all = steps(), last = step === all.length - 1; + useEffect(() => { + const key = (e: KeyboardEvent) => { if (e.key === 'Escape') onDone(); }; + addEventListener('keydown', key); + return () => removeEventListener('keydown', key); + }, [onDone]); + const s = all[step]; + return ( +
+
+

{s.title}

+

{s.body}

+
+ + + {all.map((_, i) => ( + + ))} + + {step > 0 && } + {last + ? + : } +
+
+
+ ); +} + +export function WhatsNew({ news, onClose, onSeeAll }: { news: Onboarding['news']; onClose: () => void; onSeeAll: () => void }) { + useEffect(() => { api.onboardingSeen('news').catch(() => {}); }, []); // shown once: stays up on this page until closed + return ( +
+
+ +

{t("What's new")}

+ +
+
    + {news.map(n =>
  • {t(n.news)}
  • )} +
+ +
+ ); +} diff --git a/ui/src/i18n/fr.json b/ui/src/i18n/fr.json index 9925651..75ce069 100644 --- a/ui/src/i18n/fr.json +++ b/ui/src/i18n/fr.json @@ -4115,5 +4115,19 @@ "Painted Cave": "Grotte ornée", "Horses, aurochs and hands from Lascaux on a torchlit limestone wall.": "Chevaux, aurochs et mains de Lascaux sur une paroi calcaire éclairée à la torche.", "Plain soft graphite with a little light from above.": "Un graphite doux et uni, avec un peu de lumière venue d'en haut.", - "Load any high-resolution wallpaper image via HTTP/HTTPS URL.": "Chargez n'importe quelle image haute résolution par une URL HTTP/HTTPS." + "Load any high-resolution wallpaper image via HTTP/HTTPS URL.": "Chargez n'importe quelle image haute résolution par une URL HTTP/HTTPS.", + "Welcome to Shell:B": "Bienvenue dans Shell:B", + "Shell:B is an AI assistant that runs on its own computer, not in a company’s cloud. Type a question or a task in the box at the bottom, and send it.": "Shell:B est un assistant IA qui tourne sur son propre ordinateur, pas dans le cloud d’une entreprise. Écrivez une question ou une tâche dans la zone du bas, puis envoyez-la.", + "Agents for different jobs": "Un agent pour chaque besoin", + "Each agent is good at something different: writing, research, the sea, the distant past, pictures. Pick one in the agent menu by the message box, or type @ and a name.": "Chaque agent a son point fort : l’écriture, la recherche, la mer, le passé lointain, les images. Choisissez-en un dans le menu des agents près de la zone de message, ou tapez @ suivi d’un nom.", + "Start with an example": "Commencer par un exemple", + "Below each new chat, What Shell:B can do lists everyday things to try, every agent and every app. Pick one and Shell:B takes it from there.": "Sous chaque nouvelle discussion, Ce que Shell:B sait faire propose des choses du quotidien à essayer, tous les agents et toutes les applis. Choisissez-en une et Shell:B s’occupe du reste.", + "Step {n} of {total}": "Étape {n} sur {total}", + "Show me": "Montrez-moi", + "What's new": "Nouveautés", + "See all changes": "Voir toutes les nouveautés", + "A new chat now shows what Shell:B can do: everyday tasks to try, and what each agent and app is for.": "Une nouvelle discussion montre maintenant ce que Shell:B sait faire : des tâches du quotidien à essayer, et à quoi sert chaque agent et chaque appli.", + "The Agent Lab has a Store of ready-made agents, such as an editor, a tutor and a trip planner, to install in one click.": "Le Labo des agents a une Boutique d’agents prêts à l’emploi, comme un éditeur, un tuteur ou un planificateur de voyages, à installer en un clic.", + "Thalassa, a marine biologist, can help you identify sea creatures and find the research on them.": "Thalassa, biologiste marine, peut vous aider à identifier des animaux marins et à trouver les recherches à leur sujet.", + "Shell:B can speak French: choose the language in Settings → Languages.": "Shell:B parle français : choisissez la langue dans Réglages → Langues." } diff --git a/ui/src/mobile/MobileApp.tsx b/ui/src/mobile/MobileApp.tsx index f39e16d..b63abec 100644 --- a/ui/src/mobile/MobileApp.tsx +++ b/ui/src/mobile/MobileApp.tsx @@ -940,6 +940,10 @@ export function MobileApp() { onStart={startWith} onSelectAgent={selectAgent} onOpenApp={openApp} + onOpenChangelog={() => { + setSettingsTab('changelog'); + setModal('settings'); + }} /> ) : (
diff --git a/ui/src/types.ts b/ui/src/types.ts index 1d3d94c..1609439 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -977,4 +977,6 @@ export interface UsageReport { costs: Record; } -export interface ChangelogEntry { date: string; area: string; title: string; summary: string } +export interface ChangelogEntry { date: string; area: string; title: string; summary: string; news: string } +/** server/onboarding.py: the first-visit welcome still to show, and the What's new lines not seen yet */ +export interface Onboarding { welcome: boolean; news: { date: string; title: string; news: string }[] }