@hanzo/rooms 0.1.38 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/Account.js +18 -9
- package/dist/Chat.js +122 -199
- package/dist/Column.js +16 -15
- package/dist/Directory.d.ts +4 -9
- package/dist/Directory.js +60 -155
- package/dist/Me.js +20 -16
- package/dist/Prose.js +5 -4
- package/dist/RightPane.d.ts +20 -28
- package/dist/RightPane.js +107 -327
- package/dist/Settings.js +42 -28
- package/dist/Shell.js +19 -22
- package/dist/dev.js +42 -26
- package/dist/host.d.ts +5 -9
- package/dist/host.js +5 -6
- package/dist/lib/destination.d.ts +6 -6
- package/dist/lib/destination.js +6 -6
- package/dist/lib/host.d.ts +50 -31
- package/dist/lib/host.js +98 -48
- package/dist/lib/limits.d.ts +13 -9
- package/dist/lib/limits.js +20 -0
- package/dist/lib/plans.d.ts +2 -13
- package/dist/lib/plans.js +2 -19
- package/dist/lib/session.d.ts +9 -12
- package/dist/lib/session.js +7 -19
- package/dist/pane.d.ts +4 -57
- package/dist/pane.js +13 -159
- package/package.json +3 -5
- package/dist/Billing.d.ts +0 -1
- package/dist/Billing.js +0 -32
- package/dist/enso.d.ts +0 -72
- package/dist/enso.js +0 -112
- package/dist/model.d.ts +0 -4
- package/dist/model.js +0 -44
package/dist/Chat.js
CHANGED
|
@@ -30,40 +30,36 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
30
30
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
31
31
|
import { billed, detail, familyOf, planRequired, plainly, refused } from "./failure.js";
|
|
32
32
|
import { Prose } from "./Prose.js";
|
|
33
|
-
import { useAgent, useAi, useChat,
|
|
33
|
+
import { useAgent, useAi, useChat, } from "@hanzo/ai/react";
|
|
34
34
|
import { freeCopy, grantConsent, hasConsent, isFree, } from "@hanzo/ai";
|
|
35
|
-
import { ASK, Chat as ChatSurface,
|
|
35
|
+
import { ASK, Chat as ChatSurface, Failure, Parts, words } from "@hanzo/ui/chat";
|
|
36
36
|
import { carriedIn, compose, partsOf, said as told, weigh } from "./lib/attach.js";
|
|
37
37
|
import { download, dropped, fileOf, pct, readable, refOf, retrieve } from "./lib/files.js";
|
|
38
38
|
import { channel, drop, hold, landed, localOf, spend, take, usePane, useWorkFile } from "./pane.js";
|
|
39
39
|
import { Face, brief as roomBrief, join, roleOf, roomTurn, roster as rosterLine, rosterOf, speakers, speaksOf, voiceOf } from "./cast.js";
|
|
40
|
-
import {
|
|
40
|
+
import { cleanForSpeech, speakAgent } from "./speech.js";
|
|
41
41
|
import { Crew, HOUSE, useCrew } from "./crew.js";
|
|
42
42
|
import { called } from "./team.js";
|
|
43
|
-
import { Anchor, Box, Button, Text,
|
|
44
|
-
import {
|
|
43
|
+
import { Anchor, Box, Button, Text, View, XStack, YStack } from "@hanzo/ui";
|
|
44
|
+
import { useMedia } from "@hanzo/gui";
|
|
45
45
|
import { useIam, useIamToken } from "@hanzo/iam/react";
|
|
46
|
-
import { hasSession, org } from "./lib/session.js";
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
46
|
+
import { bearer, hasSession, org } from "./lib/session.js";
|
|
47
|
+
import { api } from "./lib/api.js";
|
|
48
|
+
import { chats, house, paidPlan, paused, setCreditsAfterAllowance, useLimits } from "./lib/limits.js";
|
|
49
49
|
import { LimitedBanner } from "@hanzo/ui/product/LimitedBanner";
|
|
50
50
|
import { WEB } from "./lib/web.js";
|
|
51
51
|
import { useHydrated } from "./lib/hydrated.js";
|
|
52
52
|
import { base, served } from "./lib/ai.js";
|
|
53
53
|
import { onServed } from "./lib/served.js";
|
|
54
|
-
import { refused as worded, speech, useDictation, useTalk, useVoice
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
54
|
+
import { refused as worded, speech, useDictation, useTalk, useVoice } from "@hanzo/voice";
|
|
55
|
+
import { FileText, Image as ImageMark, Paperclip, Pin, X } from "lucide-react";
|
|
56
|
+
import { FREE } from "./lib/ai.js";
|
|
57
57
|
import { openThread, useOpen } from "./open.js";
|
|
58
58
|
import { Beside, Framed } from "./Shell.js";
|
|
59
|
-
import {
|
|
60
|
-
import { RightPane, addressOf, look, usePinned } from "./RightPane.js";
|
|
61
|
-
import { useMedia } from "@hanzo/gui";
|
|
62
|
-
import { Take } from "./copy.js";
|
|
59
|
+
import { SEED, look, useKinds } from "./RightPane.js";
|
|
63
60
|
import { Share } from "./Share.js";
|
|
64
61
|
import { useThreadActions, useThreadList } from "./thread.js";
|
|
65
|
-
import {
|
|
66
|
-
import { Enso, nameOf, useEffort } from "./enso.js";
|
|
62
|
+
import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, useDeck, useKey, useMind, usePanel, } from "@hanzo/build";
|
|
67
63
|
import { checkoutUrl } from './lib/pay.js';
|
|
68
64
|
import { Upgrade, useFree } from './Upgrade.js';
|
|
69
65
|
import { enter, LOGIN } from './lib/destination.js';
|
|
@@ -180,9 +176,6 @@ function useSeed(initial, set) {
|
|
|
180
176
|
apply.current(initial);
|
|
181
177
|
}, [initial]);
|
|
182
178
|
}
|
|
183
|
-
/** The circle both controls are cut from: @hanzo/composer's `Control`, at the
|
|
184
|
-
* 30px a line of this composer's footer row holds. */
|
|
185
|
-
const ROUND = 30;
|
|
186
179
|
export function Chat() {
|
|
187
180
|
const { isAuthenticated, isLoading } = useIam();
|
|
188
181
|
const hydrated = useHydrated();
|
|
@@ -486,10 +479,11 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
486
479
|
* that settles after mount. Picking from the menu still wins, permanently.
|
|
487
480
|
*/
|
|
488
481
|
const [asking, setAsking] = useState(null);
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
const
|
|
482
|
+
// THE MODEL AND THE EFFORT are the one choice Chat and Dev share
|
|
483
|
+
// (@hanzo/build `useMind`): the catalog from `GET /v1/models` and what the
|
|
484
|
+
// reader kept, changed from the composer's chip or from Settings.
|
|
485
|
+
const target = useMemo(() => ({ api: api(), token: bearer, org: org() }), [anonymous]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
486
|
+
const mind = useMind(target);
|
|
493
487
|
const ai = useAi();
|
|
494
488
|
// In the app's frame the composer floats on the pane as a pane of its own.
|
|
495
489
|
const framed = useContext(Framed);
|
|
@@ -506,33 +500,45 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
506
500
|
const at = channel(opened);
|
|
507
501
|
// The first of them: what a surface that addresses one person reads.
|
|
508
502
|
const withWhom = room[0] ?? null;
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
const [pinned, flipPin] = usePinned();
|
|
513
|
-
// The frame draws the column only from `$lg` (Shell.tsx `Paned`).
|
|
503
|
+
// THE SIDE PANEL (@hanzo/build `Panel`), the one Dev's runs open too: open or
|
|
504
|
+
// shut as Chat was left on a laptop, a sheet asked for on a phone, its tabs
|
|
505
|
+
// kept per conversation. The header's Panel button and ⌘. move it.
|
|
514
506
|
const wide = useMedia().lg;
|
|
507
|
+
const [panel, setPanel] = usePanel("chat", false, wide);
|
|
508
|
+
const deck = useDeck(`chat:${at}`, SEED);
|
|
509
|
+
useKey(() => setPanel(!panel));
|
|
510
|
+
// On a laptop the frame's column is the panel's; a close from the frame (its
|
|
511
|
+
// edge pulled shut, the shell's own control) comes back into `panel`.
|
|
512
|
+
useEffect(() => {
|
|
513
|
+
if (wide)
|
|
514
|
+
showAside(panel ? "tasks" : null);
|
|
515
|
+
}, [panel, wide, showAside]);
|
|
516
|
+
const wasAside = useRef(aside);
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
if (wide && wasAside.current !== aside) {
|
|
519
|
+
if (aside === null && panel)
|
|
520
|
+
setPanel(false);
|
|
521
|
+
else if (aside !== null && !panel)
|
|
522
|
+
setPanel(true);
|
|
523
|
+
}
|
|
524
|
+
wasAside.current = aside;
|
|
525
|
+
// Answers the frame's moves only.
|
|
526
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
527
|
+
}, [aside]);
|
|
528
|
+
/** What an answer wrote, open in the panel as a page rendered from its own bytes. */
|
|
529
|
+
const see = useCallback((a) => {
|
|
530
|
+
deck.open({ kind: PAGE, title: a.name, body: a.body, type: a.mime });
|
|
531
|
+
setPanel(true);
|
|
532
|
+
}, [deck, setPanel]);
|
|
515
533
|
/**
|
|
516
|
-
* OPENS A FILE A MESSAGE CARRIES, by the road the
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
* on the room — so there the file opens in a tab of the browser's own, minted
|
|
520
|
-
* before the signed address is asked for, since a window opened after an
|
|
521
|
-
* await is a popup the browser blocks.
|
|
534
|
+
* OPENS A FILE A MESSAGE CARRIES, by the road the Sources tab takes (`look`):
|
|
535
|
+
* a page tab in the side panel, the panel opened when it is shut — a column on
|
|
536
|
+
* a laptop, a sheet on a phone.
|
|
522
537
|
*/
|
|
523
538
|
const openFile = useCallback((m) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
void look(at, m, ai);
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
const tab = window.open("", "_blank");
|
|
531
|
-
if (!tab)
|
|
532
|
-
return;
|
|
533
|
-
tab.opener = null;
|
|
534
|
-
void addressOf(m, ai).then((href) => (href ? tab.location.replace(href) : tab.close()));
|
|
535
|
-
}, [wide, aside, showAside, at, ai]);
|
|
539
|
+
void look(deck, m, ai);
|
|
540
|
+
setPanel(true);
|
|
541
|
+
}, [deck, ai, setPanel]);
|
|
536
542
|
/**
|
|
537
543
|
* WHO YOU ARE TALKING TO, when it is somebody rather than the plain model.
|
|
538
544
|
*
|
|
@@ -629,20 +635,16 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
629
635
|
// A SPEECH MODEL NEVER ANSWERS A CHAT TURN. The catalog lists the
|
|
630
636
|
// transcriber and the voice beside the chat models; one that names its
|
|
631
637
|
// outputs and names no text (`chats`) is neither offered nor fallen back to.
|
|
632
|
-
const
|
|
633
|
-
const models = useMemo(() =>
|
|
638
|
+
const listed = mind.models;
|
|
639
|
+
const models = useMemo(() => listed.filter(chats), [listed]);
|
|
634
640
|
// EVERY MODEL THE GATEWAY LISTS is offered here, through the one picker, and
|
|
635
641
|
// read from the catalog's own fields (`parseModels`): its class, its family.
|
|
636
642
|
// A premium model is picked for THIS conversation and never remembered as
|
|
637
|
-
// the default, so the next chat starts on
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
// own model is sent as it is named; the gateway says which model answered
|
|
641
|
-
// (`servedBy`).
|
|
642
|
-
const listed = useMemo(() => parseModels(catalog), [catalog]);
|
|
643
|
+
// the default (`useMind`), so the next chat starts on what was kept. An
|
|
644
|
+
// agent's own model is sent as it is named; the gateway says which model
|
|
645
|
+
// answered (`servedBy`).
|
|
643
646
|
const rowOf = (id) => listed.find((m) => m.id === id);
|
|
644
|
-
const
|
|
645
|
-
const wanted = picked ?? (preferred && rowOf(preferred)?.class !== "premium" ? preferred : ENSO);
|
|
647
|
+
const wanted = mind.model;
|
|
646
648
|
// A model the catalog lists is sent as picked, whatever it answers with; only
|
|
647
649
|
// a name the catalog does not list falls to one that converses (`served`).
|
|
648
650
|
const sent = anonymous ? FREE : wanted;
|
|
@@ -652,9 +654,11 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
652
654
|
// bars and no near note — those live on Settings → Usage. It says something
|
|
653
655
|
// only once the reader is turned away: a reply answered from a free model, or
|
|
654
656
|
// a refusal.
|
|
655
|
-
const { limits, notice, reload: reread } = useLimits(!anonymous, org(), (id) => nameOf(
|
|
657
|
+
const { limits, notice, reload: reread } = useLimits(!anonymous, org(), (id) => nameOf(id, listed));
|
|
656
658
|
const held = paidPlan(limits);
|
|
657
659
|
const pause = notice && (notice.fallback || notice.refused) ? notice : null;
|
|
660
|
+
// The banner's words and ways: the plan by name, when it comes back, and the two ways on.
|
|
661
|
+
const banner = pause ? paused(pause, limits?.plan ?? "", (id) => nameOf(id, listed)) : null;
|
|
658
662
|
// EVERY MODEL IS CHOSEN, never asked for: one the plan does not cover still
|
|
659
663
|
// picks, and the gateway answers it with the policy's refusal or from Enso,
|
|
660
664
|
// which the room then shows. A turn the gateway refuses `plan_required`
|
|
@@ -662,12 +666,6 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
662
666
|
// free-lane notice.
|
|
663
667
|
const free = useFree(!anonymous) && !held;
|
|
664
668
|
const [ask, setAsk] = useState(null);
|
|
665
|
-
const choose = (id) => {
|
|
666
|
-
if (rowOf(id)?.class === "premium")
|
|
667
|
-
return setPicked(id);
|
|
668
|
-
setPicked(null);
|
|
669
|
-
setPreferred(id);
|
|
670
|
-
};
|
|
671
669
|
// KEEPING THE CONVERSATION is the reader's account, not this pane's choice:
|
|
672
670
|
// the store is per-org, so a signed-in reader has somewhere for it to go and
|
|
673
671
|
// an anonymous one does not. `thread` names the conversation being continued
|
|
@@ -684,12 +682,12 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
684
682
|
// lookups the platform's assistant uses, run inside the completion by the
|
|
685
683
|
// client's fetch (lib/web.ts), so a question about the weather or the news
|
|
686
684
|
// is answered from what the web says now.
|
|
687
|
-
params: { reasoning_effort: effort, ...(!anonymous && house(rowOf(model)?.family) ? { tools: WEB } : {}) },
|
|
685
|
+
params: { reasoning_effort: mind.effort, ...(!anonymous && house(rowOf(model)?.family) ? { tools: WEB } : {}) },
|
|
688
686
|
...(open ? { thread: open } : {}),
|
|
689
687
|
onError: (e) => {
|
|
690
688
|
const needs = planRequired(e);
|
|
691
689
|
if (needs && free)
|
|
692
|
-
setAsk({ ...needs, model: nameOf(
|
|
690
|
+
setAsk({ ...needs, model: nameOf(model, listed) });
|
|
693
691
|
},
|
|
694
692
|
});
|
|
695
693
|
const { send, set, error, thread: kept } = chat;
|
|
@@ -951,7 +949,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
951
949
|
setActWrong("");
|
|
952
950
|
if (a.kind === "switch") {
|
|
953
951
|
if (a.model)
|
|
954
|
-
|
|
952
|
+
mind.pick(a.model);
|
|
955
953
|
return;
|
|
956
954
|
}
|
|
957
955
|
if (a.kind !== "credits") {
|
|
@@ -970,9 +968,9 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
970
968
|
})
|
|
971
969
|
.catch((e) => setActWrong(e instanceof Error ? e.message : "Credits could not be turned on."));
|
|
972
970
|
},
|
|
973
|
-
// `
|
|
971
|
+
// `mind.pick` is rebuilt each render; the handler follows it.
|
|
974
972
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
975
|
-
[reread, speak, failures,
|
|
973
|
+
[reread, speak, failures, mind.model, listed]);
|
|
976
974
|
// A TURN THAT NEVER SPEAKS IS STOPPED. A stream that stays open with nothing
|
|
977
975
|
// in it would leave a Stop button and no answer for as long as the upstream
|
|
978
976
|
// holds the connection; after `STALL` it is ended and its question is said to
|
|
@@ -1005,8 +1003,31 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1005
1003
|
if (!heard || (heard.served === heard.asked && ROUTERS.has(heard.asked)))
|
|
1006
1004
|
return null;
|
|
1007
1005
|
const fell = heard.asked && heard.asked !== heard.served && !ROUTERS.has(heard.asked);
|
|
1008
|
-
return (
|
|
1006
|
+
return _jsx(Served, { served: heard.served, asked: fell ? heard.asked : undefined });
|
|
1007
|
+
};
|
|
1008
|
+
// LISTEN reads a part in its speaker's cast voice, through the platform.
|
|
1009
|
+
const listen = useCallback((who) => (text, done) => speakAgent(text, { agent: who, token, onEnd: done, onError: done }), [token]);
|
|
1010
|
+
/** The way to open what a part of an answer wrote, where it wrote something. */
|
|
1011
|
+
const opener = (text) => {
|
|
1012
|
+
const a = lead(artifacts(text));
|
|
1013
|
+
return a ? () => see(a) : undefined;
|
|
1009
1014
|
};
|
|
1015
|
+
// WHAT AN ANSWER MAKES OPENS BESIDE IT. With the panel open, a page or a
|
|
1016
|
+
// picture an answer just finished writing is opened as a tab; with it shut,
|
|
1017
|
+
// the answer's own Open does it.
|
|
1018
|
+
const writing = useRef(false);
|
|
1019
|
+
useEffect(() => {
|
|
1020
|
+
const was = writing.current;
|
|
1021
|
+
writing.current = chat.streaming;
|
|
1022
|
+
if (!was || chat.streaming || !panel)
|
|
1023
|
+
return;
|
|
1024
|
+
const answer = [...turns.current].reverse().find((m) => m.role === "assistant");
|
|
1025
|
+
const made = typeof answer?.content === "string" ? artifacts(answer.content).filter(renders) : [];
|
|
1026
|
+
if (made.length)
|
|
1027
|
+
see(made[made.length - 1]);
|
|
1028
|
+
// On the turn's end only.
|
|
1029
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1030
|
+
}, [chat.streaming]);
|
|
1010
1031
|
// WHAT WAS SAID, which is not everything the thread holds. The persona rides
|
|
1011
1032
|
// as a system turn so the wire carries it; drawing it would print a character
|
|
1012
1033
|
// brief in the transcript as though somebody had typed it, and would make a
|
|
@@ -1021,6 +1042,8 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1021
1042
|
// record's own picture. Its own answer comes first, which is the order
|
|
1022
1043
|
// `Face` already states for every other surface.
|
|
1023
1044
|
const portrait = mate?.avatar ?? persona?.avatar ?? roleOf(withWhom)?.portrait;
|
|
1045
|
+
// Chat's kinds of tab in the side panel: Artifacts and Sources, read from the turns.
|
|
1046
|
+
const kinds = useKinds({ at, said, held: files, deck });
|
|
1024
1047
|
/** Whether the field has focus, so the FRAME can wear the ring the field is
|
|
1025
1048
|
* no longer allowed to draw. `:focus-within` would say this in CSS; the
|
|
1026
1049
|
* stylesheet that would hold it cannot reach past gui's own rule, so the
|
|
@@ -1139,17 +1162,17 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1139
1162
|
const said = !failed
|
|
1140
1163
|
? "No answer came back for this question."
|
|
1141
1164
|
: failed.stalled
|
|
1142
|
-
? `${nameOf(
|
|
1165
|
+
? `${nameOf(model, listed)} sent nothing for ${STALL / 60_000} minutes, so the turn was stopped.`
|
|
1143
1166
|
: failed.empty
|
|
1144
|
-
? `${nameOf(
|
|
1167
|
+
? `${nameOf(model, listed)} answered with nothing.`
|
|
1145
1168
|
: bill
|
|
1146
|
-
? notice?.message ?? (bill.message || plainly(wrong, "this conversation"))
|
|
1169
|
+
? banner?.message ?? notice?.message ?? (bill.message || plainly(wrong, "this conversation"))
|
|
1147
1170
|
: resign
|
|
1148
1171
|
? anonymous
|
|
1149
1172
|
? "Sign in to ask this."
|
|
1150
1173
|
: "Your session needs to be refreshed. Sign in to reconnect."
|
|
1151
1174
|
: how?.provider
|
|
1152
|
-
? `${familyOf(model) || nameOf(
|
|
1175
|
+
? `${familyOf(model) || nameOf(model, listed)} is unavailable right now.`
|
|
1153
1176
|
: plainly(wrong, "this conversation");
|
|
1154
1177
|
return (_jsx(Failure, { onRetry: resign || bill?.code === "free_plan_cap" ? undefined : () => speak(text, question.id), children: _jsxs(YStack, { gap: "$2", children: [_jsxs(Text, { fontSize: "$3", color: "$ink", children: [whom.length ? `No answer from ${join(whom)}. ` : "", said] }), how?.provider && wrong ? _jsx(Why, { text: detail(wrong) }) : null, !bill && (resign || lift || how?.wallet || how?.visitor) ? (_jsx(XStack, { gap: "$2", flexWrap: "wrap", children: resign ? (_jsx(Button, { size: "sm", onClick: () => {
|
|
1155
1178
|
if (typeof window !== "undefined") {
|
|
@@ -1157,7 +1180,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1157
1180
|
localStorage.removeItem("hanzo_iam_refresh_token");
|
|
1158
1181
|
}
|
|
1159
1182
|
enter(LOGIN);
|
|
1160
|
-
}, children: "Sign in" })) : lift ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => setAsk({ ...lift, model: nameOf(
|
|
1183
|
+
}, children: "Sign in" })) : lift ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => setAsk({ ...lift, model: nameOf(model, listed) }), children: "Upgrade" })) : how?.visitor ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => enter(), children: "Sign in to keep going" })) : (_jsx(Anchor, { href: checkoutUrl(), children: _jsx(Button, { size: "sm", variant: "outline", children: "Add credit" }) })) })) : null] }) }));
|
|
1161
1184
|
};
|
|
1162
1185
|
// The agreement is asked OVER the room, not instead of it. Returning the
|
|
1163
1186
|
// consent on its own replaced the whole pane: the question the reader had
|
|
@@ -1203,22 +1226,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1203
1226
|
e.preventDefault();
|
|
1204
1227
|
// A dropped folder arrives as entries; each file under it goes up with its path.
|
|
1205
1228
|
void dropped(e.dataTransfer).then((list) => hold(at, list, via));
|
|
1206
|
-
}, children: [dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { height: 48, items: "center", justify: "space-between", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Box, { render: "button", onClick: () => mine && void acts.pin(mine), "aria-disabled": !mine, "aria-label": mine?.pinned ? "Unpin this conversation" : "Pin this conversation", "aria-pressed": !!mine?.pinned, p: "$1.5", rounded: "$2", opacity: mine ? 1 : 0.5, hoverStyle: { bg: '$hover' }, children: _jsx(Pin, { size: 15, "aria-hidden": true, color: mine?.pinned ? "var(--foreground)" : "var(--muted-foreground)", fill: mine?.pinned ? "currentColor" : "none" }) }), room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { items: "center", gap: "$
|
|
1207
|
-
if (aside === null) {
|
|
1208
|
-
showAside("tasks");
|
|
1209
|
-
if (!pinned)
|
|
1210
|
-
flipPin();
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
flipPin();
|
|
1214
|
-
}, "aria-label": "Toggle pinned summary", "aria-pressed": aside !== null && pinned,
|
|
1215
|
-
// NOT ON A PHONE, because there is nothing for it to show there.
|
|
1216
|
-
// The frame draws the column only above `$lg` — it spends the whole
|
|
1217
|
-
// width on the room below that (`Shell.tsx`) — so this control
|
|
1218
|
-
// would press, change the stored pin, and move nothing on screen.
|
|
1219
|
-
// A button whose effect is invisible is a button that does nothing.
|
|
1220
|
-
// Same breakpoint as the column, so the two cannot drift apart.
|
|
1221
|
-
display: "none", $lg: { display: "flex" }, p: "$2", rounded: "$2", hoverStyle: { bg: "$hover" }, children: _jsx(PanelRight, { size: 15, color: "var(--soft, var(--muted-foreground))" }) }), open ? _jsx(Share, { thread: open }) : null] })] }), _jsxs(Beside, { children: [_jsx(RightPane, { said: said, onClose: () => showAside(null) }), _jsx(Work, { persona: persona ?? null, room: room, model: model, messages: said })] }), isFree(model) ? (_jsxs(Text, { fontSize: "$2", color: "$soft", text: "center", maxW: MEASURE, mx: "auto", px: "$4", py: "$2", children: [freeCopy.notice, " ", _jsx(Anchor, { href: site("/legal/privacy"), children: freeCopy.noticeCta })] })) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
|
|
1229
|
+
}, children: [dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { height: 48, items: "center", justify: "space-between", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Box, { render: "button", onClick: () => mine && void acts.pin(mine), "aria-disabled": !mine, "aria-label": mine?.pinned ? "Unpin this conversation" : "Pin this conversation", "aria-pressed": !!mine?.pinned, p: "$1.5", rounded: "$2", opacity: mine ? 1 : 0.5, hoverStyle: { bg: '$hover' }, children: _jsx(Pin, { size: 15, "aria-hidden": true, color: mine?.pinned ? "var(--foreground)" : "var(--muted-foreground)", fill: mine?.pinned ? "currentColor" : "none" }) }), room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { items: "center", gap: "$2", children: [open ? _jsx(Share, { thread: open }) : null, _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: kinds, onHide: () => setPanel(false), label: "Conversation details" }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: kinds, sheet: true, onHide: () => setPanel(false), label: "Conversation details" }) : null, isFree(model) ? (_jsxs(Text, { fontSize: "$2", color: "$soft", text: "center", maxW: MEASURE, mx: "auto", px: "$4", py: "$2", children: [freeCopy.notice, " ", _jsx(Anchor, { href: site("/legal/privacy"), children: freeCopy.noticeCta })] })) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
|
|
1222
1230
|
// AN EMPTY ROOM CENTRES ITS COMPOSER. While nothing has been said the
|
|
1223
1231
|
// opening (the heading and the starters) and the composer sit as one
|
|
1224
1232
|
// group in the middle of the height, with or without the plan bars in
|
|
@@ -1270,9 +1278,9 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1270
1278
|
const silent = room.length > 1 && !(chat.streaming && at === shown.length - 1)
|
|
1271
1279
|
? addressed(words(q?.content), room).filter((name) => !parts.some((part) => part.who?.toLowerCase() === name.toLowerCase()))
|
|
1272
1280
|
: [];
|
|
1273
|
-
return (_jsxs(YStack, { gap: "$3", children: [parts.map((part, i) => (_jsxs(YStack, { gap: "$1", children: [part.who ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Face, { src: faces[part.who]?.avatar, emoji: faces[part.who]?.emoji, name: part.who, size: 20 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: part.who })] })) : null, _jsx(Prose, { text: part.text }), part.text ?
|
|
1281
|
+
return (_jsxs(YStack, { gap: "$3", children: [parts.map((part, i) => (_jsxs(YStack, { gap: "$1", children: [part.who ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Face, { src: faces[part.who]?.avatar, emoji: faces[part.who]?.emoji, name: part.who, size: 20 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: part.who })] })) : null, _jsx(Prose, { text: part.text }), part.text ? _jsx(Reply, { text: part.text, listen: listen(part.who ?? withWhom ?? undefined), onOpen: opener(part.text) }) : null] }, i))), silent.length ? (_jsxs(Text, { fontSize: "$2", color: "$soft", children: ["No answer from ", join(silent), " in this reply."] })) : null, byline(turn.id)] }));
|
|
1274
1282
|
}
|
|
1275
|
-
return (_jsxs(YStack, { gap: "$1", children: [_jsx(Prose, { text: said }), turn.role === "assistant" && said ?
|
|
1283
|
+
return (_jsxs(YStack, { gap: "$1", children: [_jsx(Prose, { text: said }), turn.role === "assistant" && said ? _jsx(Reply, { text: said, listen: listen(withWhom ?? undefined), onOpen: opener(said) }) : null, turn.role === "assistant" && said ? byline(turn.id) : null] }));
|
|
1276
1284
|
},
|
|
1277
1285
|
// The page's ONE heading, and it belongs to the empty transcript rather
|
|
1278
1286
|
// than to a signed-out branch — the branch is gone, and the heading went
|
|
@@ -1284,61 +1292,20 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1284
1292
|
}, px: "$3", py: "$2", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: "$raised" }, children: _jsx(Text, { fontSize: "$2", color: "$soft", children: one }) }, one))) })] })),
|
|
1285
1293
|
// The transcript reads at the same measure. `Thread` defaults to 768
|
|
1286
1294
|
// itself; handing it the column here is what lets Width reach it.
|
|
1287
|
-
column: { maxW: MEASURE }, composer: {
|
|
1295
|
+
column: { maxW: MEASURE }, composer: prompt({
|
|
1288
1296
|
value: draft,
|
|
1289
1297
|
onChange: setDraft,
|
|
1290
1298
|
onSend: submit,
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
//
|
|
1294
|
-
|
|
1299
|
+
onStop: chat.stop,
|
|
1300
|
+
busy: chat.streaming,
|
|
1301
|
+
// Files alone are a message: what is held goes with an empty draft too.
|
|
1302
|
+
ready: Boolean(draft.trim() || files.length),
|
|
1303
|
+
// A CHARACTER IS WHO YOU ARE WRITING TO, so the field says so, and
|
|
1304
|
+
// `label` keeps the accessible name constant while it does.
|
|
1295
1305
|
placeholder: room.length ? `Message ${join(room)}` : placeholder,
|
|
1296
1306
|
label: ASK,
|
|
1297
|
-
//
|
|
1298
|
-
|
|
1299
|
-
// because with a foot the composer is a shell around this frame and
|
|
1300
|
-
// a margin given here would land inside the shell.
|
|
1301
|
-
// THE SAME MEASURE THE REST OF THIS FILE READS AT. The transcript's
|
|
1302
|
-
// text sits at the measure less its own 0.75rem gutters, so the field
|
|
1303
|
-
// is that wide too and its edges line up with the words above it.
|
|
1304
|
-
width: "calc(100% - 32px)",
|
|
1305
|
-
maxW: `calc(${MEASURE} - 1.5rem)`,
|
|
1306
|
-
mx: "auto",
|
|
1307
|
-
...(framed ? pane() : null),
|
|
1308
|
-
// THE COMPONENT'S OWN SHAPE, and nothing here reaching into it.
|
|
1309
|
-
//
|
|
1310
|
-
// This carried about twenty style props that rebuilt it into a single
|
|
1311
|
-
// capsule: `flexDirection: "row"` above all, which lays the footer row
|
|
1312
|
-
// BESIDE a `flex: 1` field instead of under it. The footer row has no
|
|
1313
|
-
// width of its own in that arrangement, so send left the viewport at
|
|
1314
|
-
// every size but the widest — measured at 1280 it ended at 1315, at
|
|
1315
|
-
// 834 at 902, at 390 at 466, and on a phone the field stood 200px tall
|
|
1316
|
-
// where a line is 24. Each override was answering damage from the one
|
|
1317
|
-
// before it.
|
|
1318
|
-
//
|
|
1319
|
-
// Composer is a field over a footer row. CHOOSING a model is a
|
|
1320
|
-
// preference and not part of every message, so Settings owns the
|
|
1321
|
-
// choice — but the row still NAMES what is about to answer, because
|
|
1322
|
-
// `served()` may not send what was asked for.
|
|
1323
|
-
//
|
|
1324
|
-
// That fallback is written against this: a preference the gateway
|
|
1325
|
-
// does not carry gives way to a model it does, "because a chat that
|
|
1326
|
-
// works on a model the reader can see beats a chat that refuses on a
|
|
1327
|
-
// name nobody serves". Measured on production, neither `enso` nor
|
|
1328
|
-
// `free` is in the catalogue — every turn lands on the first served
|
|
1329
|
-
// model — so without this the substitution happens in silence and the
|
|
1330
|
-
// sentence justifying it is not true.
|
|
1331
|
-
//
|
|
1332
|
-
// Pressing it opens the pane that owns the choice, which is the one
|
|
1333
|
-
// place to change it.
|
|
1334
|
-
// Everything that is not the field, at the end of the row: Enso, the
|
|
1335
|
-
// mic, send. Send is drawn here because this slot replaces the
|
|
1336
|
-
// package's button and `streaming` still has to become stop; it keeps
|
|
1337
|
-
// `data-slot="composer-send"` so a driver finds it, and it is the same
|
|
1338
|
-
// `Control` circle as the mic, filled.
|
|
1339
|
-
// THE PAPERCLIP is the column's "Attach files", one press closer: the
|
|
1340
|
-
// same `take`, into the same `hold`.
|
|
1341
|
-
children: (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { children: _jsx(Control, { type: "button", size: ROUND, "aria-label": "Attach files", onClick: () => take(at, via), children: _jsx(Paperclip, { size: 15, "aria-hidden": true }) }) }), _jsx(TooltipContent, { children: _jsx(Text, { fontSize: "$2", children: "Attach files" }) })] })),
|
|
1307
|
+
// In the app's frame the composer floats on the pane as a pane of its own.
|
|
1308
|
+
framed,
|
|
1342
1309
|
// A file or an image pasted into the field is held like a picked one;
|
|
1343
1310
|
// words paste as words.
|
|
1344
1311
|
field: {
|
|
@@ -1349,70 +1316,26 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1349
1316
|
void hold(at, Array.from(e.clipboardData.files), via);
|
|
1350
1317
|
},
|
|
1351
1318
|
},
|
|
1352
|
-
// WHAT GOES WITH THE MESSAGE, above the frame
|
|
1353
|
-
//
|
|
1354
|
-
head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(
|
|
1319
|
+
// WHAT GOES WITH THE MESSAGE, above the frame and at its width: the
|
|
1320
|
+
// pause and its ways past, one chip a file, and talk mode's words.
|
|
1321
|
+
head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(_Fragment, { children: [banner ? (_jsx(LimitedBanner, { message: banner.message, actions: banner.actions, onAction: act, onUsage: () => showSettings("usage") })) : null, actWrong ? (_jsx(Text, { role: "alert", "data-slot": "limited-banner-wrong", fontSize: "$2", color: BAD, children: actWrong })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onOpen: openFile, onRemove: () => drop(at, one.id) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
|
|
1355
1322
|
? worded(talk.refusal)
|
|
1356
1323
|
: talk.reply
|
|
1357
1324
|
? `Hanzo: ${talk.reply}`
|
|
1358
1325
|
: talk.state === "listening"
|
|
1359
1326
|
? "Listening — speak, and Hanzo answers aloud."
|
|
1360
1327
|
: "Connecting…" })) : null] })) : undefined,
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1328
|
+
// THE PAPERCLIP is the Sources tab's "Attach files", one press closer:
|
|
1329
|
+
// the same `take`, into the same `hold`.
|
|
1330
|
+
onAttach: () => take(at, via),
|
|
1331
|
+
// The one model-and-effort chip Dev's composer has (@hanzo/build).
|
|
1332
|
+
mind,
|
|
1333
|
+
limits,
|
|
1334
|
+
voice,
|
|
1335
|
+
talk,
|
|
1336
|
+
}) }), said.length === 0 ? _jsx(Crew, {}) : null, isFree(model) ? (_jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models. ", _jsx(Anchor, { href: site("/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] }) })) : null, consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
|
|
1367
1337
|
}
|
|
1368
1338
|
/** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
|
|
1369
1339
|
function Consent({ question, onAgree, onCancel, }) {
|
|
1370
1340
|
return (_jsx(YStack, { position: "absolute", t: 0, l: 0, r: 0, b: 0, items: "center", justify: "center", p: "$4", z: 40, bg: SCRIM, children: _jsxs(YStack, { width: "100%", maxW: 512, px: "$5", py: "$5", gap: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Text, { render: "h2", fontSize: "$6", fontWeight: "500", color: "$ink", children: freeCopy.consentTitle }), question ? (_jsxs(Text, { fontSize: "$3", color: "$soft", numberOfLines: 2, children: ["Your question is waiting: \u201C", question, "\u201D"] })) : null, _jsx(Text, { fontSize: "$4", color: "$soft", children: freeCopy.consentBody }), _jsx(YStack, { gap: "$2", children: freeCopy.consentPoints.map((point) => (_jsx(Text, { fontSize: "$3", color: "$soft", children: point }, point))) }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", children: [_jsx(Button, { onClick: onAgree, children: freeCopy.consentAgreeCta }), _jsx(Button, { variant: "outline", onClick: onCancel, children: freeCopy.consentCancelCta })] }), _jsxs(Text, { fontSize: "$2", color: "$soft", children: [freeCopy.termsText, " ", _jsx(Anchor, { href: site("/legal/terms"), children: "Terms" }), " · ", _jsx(Anchor, { href: site("/legal/privacy"), children: "Privacy" })] })] }) }));
|
|
1371
1341
|
}
|
|
1372
|
-
function Work({ persona, room, model, messages, }) {
|
|
1373
|
-
const done = useMemo(() => {
|
|
1374
|
-
// A result arrives as its own turn, keyed back to the call that asked for
|
|
1375
|
-
// it — so the pairing is a lookup and not an assumption about order.
|
|
1376
|
-
const back = new Map();
|
|
1377
|
-
for (const m of messages) {
|
|
1378
|
-
if (m.role === "tool" && m.tool_call_id) {
|
|
1379
|
-
back.set(m.tool_call_id, typeof m.content === "string" ? m.content : "");
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
return messages
|
|
1383
|
-
.flatMap((m) => m.tool_calls ?? [])
|
|
1384
|
-
.map((call) => ({
|
|
1385
|
-
id: call.id,
|
|
1386
|
-
name: call.function.name,
|
|
1387
|
-
asked: call.function.arguments,
|
|
1388
|
-
got: back.get(call.id) ?? null,
|
|
1389
|
-
}));
|
|
1390
|
-
}, [messages]);
|
|
1391
|
-
/**
|
|
1392
|
-
* WHAT THE CONVERSATION MADE.
|
|
1393
|
-
*
|
|
1394
|
-
* A fenced block in an answer is a thing rather than a sentence — a file, a
|
|
1395
|
-
* command, a page — and read inline it scrolls away with the talk. Here it
|
|
1396
|
-
* keeps a label and a copy control, which is what somebody came to a code
|
|
1397
|
-
* block to do.
|
|
1398
|
-
*
|
|
1399
|
-
* Read from the turns rather than tracked as it arrives: the transcript is
|
|
1400
|
-
* already the record, and a second list of what was made would be a second
|
|
1401
|
-
* thing to keep in step with it.
|
|
1402
|
-
*/
|
|
1403
|
-
const made = useMemo(() => {
|
|
1404
|
-
const out = [];
|
|
1405
|
-
for (const m of messages) {
|
|
1406
|
-
if (m.role !== "assistant")
|
|
1407
|
-
continue;
|
|
1408
|
-
const text = typeof m.content === "string" ? m.content : "";
|
|
1409
|
-
for (const block of text.matchAll(/```([\w.+-]*)\n([\s\S]*?)```/g)) {
|
|
1410
|
-
const code = block[2].replace(/\s+$/, "");
|
|
1411
|
-
if (code)
|
|
1412
|
-
out.push({ id: `${m.id}-${out.length}`, language: block[1] || "text", code });
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
return out;
|
|
1416
|
-
}, [messages]);
|
|
1417
|
-
return (_jsxs(YStack, { p: "$3", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", textTransform: "uppercase", children: "Answering" }), room.length ? (room.map((name) => (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Face, { name: name, size: 22 }), _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: name })] }, name)))) : (_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: model })), room.length === 1 && persona ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 3, children: persona.description || model })) : null] }), made.length > 0 ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", textTransform: "uppercase", children: "Artifacts" }), made.map((one) => (_jsx(Code, { language: one.language, value: one.code, children: one.code }, one.id)))] })) : null, _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", textTransform: "uppercase", children: "Work" }), done.length === 0 ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: "Nothing was looked up for this conversation yet." })) : (done.map((call) => (_jsxs(YStack, { gap: "$1", p: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", color: "$ink", children: call.name }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 2, children: call.asked }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 6, children: call.got ?? "Running…" })] }, call.id))))] })] }));
|
|
1418
|
-
}
|