@fadenstack/ui-web 0.0.0-stage → 0.3.0-rc.1
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/LICENSE +187 -0
- package/NOTICE +19 -0
- package/README.md +24 -2
- package/dist/avatar.d.ts +30 -0
- package/dist/avatar.d.ts.map +1 -0
- package/dist/avatar.js +85 -0
- package/dist/avatar.js.map +1 -0
- package/dist/dom.d.ts +43 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +113 -0
- package/dist/dom.js.map +1 -0
- package/dist/icons.d.ts +38 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +37 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation.d.ts +25 -0
- package/dist/presentation.d.ts.map +1 -0
- package/dist/presentation.js +46 -0
- package/dist/presentation.js.map +1 -0
- package/package.json +44 -4
- package/src/avatar.ts +119 -0
- package/src/dom.ts +118 -0
- package/src/icons.ts +38 -0
- package/src/index.ts +26 -0
- package/src/presentation.ts +60 -0
- package/styles.css +1440 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.js","sourceRoot":"","sources":["../src/icons.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,IAAI,EAAE,qDAAqD;IAC3D,KAAK,EAAE,8GAA8G;IACrH,MAAM,EAAE,4IAA4I;IACpJ,SAAS,EAAE,4QAA4Q;IACvR,IAAI,EAAE,eAAe;IACrB,IAAI,EAAE,kCAAkC;IACxC,OAAO,EAAE,8HAA8H;IACvI,OAAO,EAAE,0CAA0C;IACnD,IAAI,EAAE,sHAAsH;IAC5H,KAAK,EAAE,6CAA6C;IACpD,KAAK,EAAE,2FAA2F;IAClG,IAAI,EAAE,0KAA0K;IAChL,OAAO,EAAE,2CAA2C;IACpD,MAAM,EAAE,mHAAmH;IAC3H,IAAI,EAAE,sJAAsJ;IAC5J,4CAA4C;IAC5C,QAAQ,EAAE,6CAA6C;IACvD,IAAI,EAAE,uHAAuH;IAC7H,KAAK,EAAE,6GAA6G;IACpH,KAAK,EAAE,yEAAyE;IAChF,OAAO,EAAE,2DAA2D;IACpE,IAAI,EAAE,4EAA4E;IAClF,KAAK,EAAE,mRAAmR;IAC1R,KAAK,EAAE,0CAA0C;IACjD,IAAI,EAAE,iGAAiG;IACvG,MAAM,EAAE,yIAAyI;IACjJ,IAAI,EAAE,2FAA2F;IACjG,QAAQ,EAAE,+nBAA+nB;CACjoB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { iconPaths, type IconName } from "./icons.js";
|
|
2
|
+
export { agentAvatar, type AgentAvatarOptions, type SvgNode } from "./avatar.js";
|
|
3
|
+
export { COPIED_MS, composerKey, dismissOnOutside, fitToContent, focusAtEnd, picturesIn, readPicture, stickToBottom, type ComposerAction, type ReadPicture, } from "./dom.js";
|
|
4
|
+
export { approvalIcon, diffMarker, headingTag, noticeIcon, noticeRole, toolBadges, toolClassName, toolStatusIcon, turnMeta, type Badge, } from "./presentation.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EACL,SAAS,EACT,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,UAAU,EACV,WAAW,EACX,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,YAAY,EACZ,UAAU,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,aAAa,EACb,cAAc,EACd,QAAQ,EACR,KAAK,KAAK,GACX,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { iconPaths } from "./icons.js";
|
|
2
|
+
export { agentAvatar } from "./avatar.js";
|
|
3
|
+
export { COPIED_MS, composerKey, dismissOnOutside, fitToContent, focusAtEnd, picturesIn, readPicture, stickToBottom, } from "./dom.js";
|
|
4
|
+
export { approvalIcon, diffMarker, headingTag, noticeIcon, noticeRole, toolBadges, toolClassName, toolStatusIcon, turnMeta, } from "./presentation.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAiB,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,WAAW,EAAyC,MAAM,aAAa,CAAC;AACjF,OAAO,EACL,SAAS,EACT,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,UAAU,EACV,WAAW,EACX,aAAa,GAGd,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,YAAY,EACZ,UAAU,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,aAAa,EACb,cAAc,EACd,QAAQ,GAET,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ApprovalPart, DiffLine, NoticeSeverity, ToolCallPart, TurnViewModel, UiStrings } from "@fadenstack/ui";
|
|
2
|
+
import type { IconName } from "./icons.js";
|
|
3
|
+
/** How the parts look, decided once for every skin: classes, icons and badges the stylesheet expects. */
|
|
4
|
+
/** The tool line's classes: failed, or not run (denied, stopped). */
|
|
5
|
+
export declare function toolClassName(part: ToolCallPart): string;
|
|
6
|
+
/** The tool line's status icon; none while it runs (a skin shows its spinner). */
|
|
7
|
+
export declare function toolStatusIcon(part: ToolCallPart): IconName | undefined;
|
|
8
|
+
/** A badge: its text and classes. */
|
|
9
|
+
export interface Badge {
|
|
10
|
+
text: string;
|
|
11
|
+
className: string;
|
|
12
|
+
}
|
|
13
|
+
/** The tool line's badges: where the tool runs when not here, and what a tool here does when it changes something. */
|
|
14
|
+
export declare function toolBadges(part: ToolCallPart, strings: UiStrings): Badge[];
|
|
15
|
+
export declare function approvalIcon(part: ApprovalPart): IconName;
|
|
16
|
+
export declare function noticeIcon(severity: NoticeSeverity): IconName;
|
|
17
|
+
/** A notice is read out at once unless it only informs. */
|
|
18
|
+
export declare function noticeRole(severity: NoticeSeverity): "note" | "alert";
|
|
19
|
+
/** The mark before a diff line. */
|
|
20
|
+
export declare function diffMarker(kind: DiffLine["kind"]): string;
|
|
21
|
+
/** An answer's heading: two levels below the panel's own headings, h6 at most. */
|
|
22
|
+
export declare function headingTag(level: number): "h3" | "h4" | "h5" | "h6";
|
|
23
|
+
/** The line under a finished answer: how long it took and what it used. */
|
|
24
|
+
export declare function turnMeta(turn: TurnViewModel): string;
|
|
25
|
+
//# sourceMappingURL=presentation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presentation.d.ts","sourceRoot":"","sources":["../src/presentation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAErH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,yGAAyG;AAEzG,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAIxD;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,GAAG,SAAS,CAIvE;AAED,qCAAqC;AACrC,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,sHAAsH;AACtH,wBAAgB,UAAU,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,GAAG,KAAK,EAAE,CAI1E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAEzD;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,cAAc,GAAG,QAAQ,CAE7D;AAED,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,GAAG,OAAO,CAErE;AAED,mCAAmC;AACnC,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,CAEzD;AAED,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAEnE;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAEpD"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** How the parts look, decided once for every skin: classes, icons and badges the stylesheet expects. */
|
|
2
|
+
/** The tool line's classes: failed, or not run (denied, stopped). */
|
|
3
|
+
export function toolClassName(part) {
|
|
4
|
+
const failed = part.state === "failed";
|
|
5
|
+
const denied = part.state === "denied" || part.state === "cancelled";
|
|
6
|
+
return `fdn-tool${failed ? " fdn-tool-failed" : ""}${denied ? " fdn-tool-denied" : ""}`;
|
|
7
|
+
}
|
|
8
|
+
/** The tool line's status icon; none while it runs (a skin shows its spinner). */
|
|
9
|
+
export function toolStatusIcon(part) {
|
|
10
|
+
if (part.isRunning)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (part.state === "failed")
|
|
13
|
+
return "warning";
|
|
14
|
+
return part.state === "denied" || part.state === "cancelled" ? "close" : "check";
|
|
15
|
+
}
|
|
16
|
+
/** The tool line's badges: where the tool runs when not here, and what a tool here does when it changes something. */
|
|
17
|
+
export function toolBadges(part, strings) {
|
|
18
|
+
if (part.source !== "host")
|
|
19
|
+
return [{ text: part.source === "gateway" ? strings.serverTool : part.source, className: "fdn-badge" }];
|
|
20
|
+
if (part.toolClass !== "read")
|
|
21
|
+
return [{ text: part.classLabel, className: `fdn-badge fdn-class-${part.toolClass}` }];
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
export function approvalIcon(part) {
|
|
25
|
+
return part.isDangerous ? "warning" : "shield";
|
|
26
|
+
}
|
|
27
|
+
export function noticeIcon(severity) {
|
|
28
|
+
return severity === "info" ? "info" : severity === "blocked" ? "shield" : "warning";
|
|
29
|
+
}
|
|
30
|
+
/** A notice is read out at once unless it only informs. */
|
|
31
|
+
export function noticeRole(severity) {
|
|
32
|
+
return severity === "info" ? "note" : "alert";
|
|
33
|
+
}
|
|
34
|
+
/** The mark before a diff line. */
|
|
35
|
+
export function diffMarker(kind) {
|
|
36
|
+
return kind === "added" ? "+ " : kind === "removed" ? "- " : " ";
|
|
37
|
+
}
|
|
38
|
+
/** An answer's heading: two levels below the panel's own headings, h6 at most. */
|
|
39
|
+
export function headingTag(level) {
|
|
40
|
+
return `h${Math.min(6, level + 2)}`;
|
|
41
|
+
}
|
|
42
|
+
/** The line under a finished answer: how long it took and what it used. */
|
|
43
|
+
export function turnMeta(turn) {
|
|
44
|
+
return [turn.durationText, turn.usageText].filter(Boolean).join(" · ");
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=presentation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presentation.js","sourceRoot":"","sources":["../src/presentation.ts"],"names":[],"mappings":"AAIA,yGAAyG;AAEzG,qEAAqE;AACrE,MAAM,UAAU,aAAa,CAAC,IAAkB;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC;IACrE,OAAO,WAAW,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAC1F,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAAC,IAAkB;IAC/C,IAAI,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACrC,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC9C,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AACnF,CAAC;AAQD,sHAAsH;AACtH,MAAM,UAAU,UAAU,CAAC,IAAkB,EAAE,OAAkB;IAC/D,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;IACpI,IAAI,IAAI,CAAC,SAAS,KAAK,MAAM;QAAE,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,uBAAuB,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACtH,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAkB;IAC7C,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,QAAwB;IACjD,OAAO,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AACtF,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,UAAU,CAAC,QAAwB;IACjD,OAAO,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;AAChD,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,UAAU,CAAC,IAAsB;IAC/C,OAAO,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACpE,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,EAA+B,CAAC;AACnE,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAC,IAAmB;IAC1C,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACzE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fadenstack/ui-web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0-rc.1",
|
|
4
|
+
"description": "The web look of the Fadenstack chat for any framework: the stylesheet, icons, the agent's avatar and the panel's browser behaviours",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Emagin8 UG (haftungsbeschränkt)",
|
|
7
|
+
"homepage": "https://developers.fadenstack.com/sdk/typescript/",
|
|
8
|
+
"bugs": "https://github.com/fadenstack/fadenstack-sdk-typescript/issues",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/fadenstack/fadenstack-sdk-typescript.git",
|
|
12
|
+
"directory": "packages/ui-web"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"fadenstack",
|
|
16
|
+
"ai",
|
|
17
|
+
"agents",
|
|
18
|
+
"chat",
|
|
19
|
+
"css"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": [
|
|
23
|
+
"*.css"
|
|
24
|
+
],
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./styles.css": "./styles.css"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"styles.css",
|
|
38
|
+
"NOTICE"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@fadenstack/ui": "0.3.0-rc.1"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/avatar.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent's face: the Fadenstack "stack" -- three plates, the rose middle one is the thread, and two eyes peek
|
|
3
|
+
* out from under the top plate. Drawn from the brand geometry (brand/avatar/make_agent_avatar.py) as data, so
|
|
4
|
+
* every skin draws the same SVG; it scales and follows the panel's theme tokens (`--fdn-agent-*`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** An SVG element as data, which a skin turns into its own elements (React's `createElement`, Vue's `h`). */
|
|
8
|
+
export interface SvgNode {
|
|
9
|
+
tag: string;
|
|
10
|
+
/** Attributes as SVG spells them ("stroke-width", "viewBox", "class"), in the order they are written. */
|
|
11
|
+
attrs: Readonly<Record<string, string | number>>;
|
|
12
|
+
/** Inline style: camelCase properties and custom properties ("--fdn-agent-thread"). */
|
|
13
|
+
style?: Readonly<Record<string, string | number>>;
|
|
14
|
+
children?: readonly SvgNode[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentAvatarOptions {
|
|
18
|
+
/** Pixels. Up to 32 px the small cut is drawn: bigger eyes, the top plate raised, no loose thread end. */
|
|
19
|
+
size?: number;
|
|
20
|
+
/** `mono` draws one colour (`currentColor`): eyes are rings with a pupil, overlaps are transparent gaps. */
|
|
21
|
+
variant?: "color" | "mono";
|
|
22
|
+
/** A custom agent's own thread colour for the middle plate. Fadenstack's own agents keep the rose. */
|
|
23
|
+
color?: string;
|
|
24
|
+
/** Read out by screen readers; without it the avatar is decorative. */
|
|
25
|
+
label?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Unique on the page (React's `useId`, Vue's `useId`): the mono variant's masks are named after it. */
|
|
28
|
+
idPrefix: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const W = 58, H = 29, T = 9, MID = 122, BOT = 146;
|
|
32
|
+
const TAIL = "M152 128 C168 132 176 148 168 160";
|
|
33
|
+
const EYE_Y = 104;
|
|
34
|
+
|
|
35
|
+
const CUTS = {
|
|
36
|
+
full: { view: "21 30 158 158", lid: 62, eyes: [88, 112], rx: 8, ry: 9.5, outline: 2, pupil: 4.2, glint: 1.4, ring: 3.6, mpupil: 4.6, gap: 4 },
|
|
37
|
+
small: { view: "19.5 25 161 161", lid: 56, eyes: [84, 116], rx: 11, ry: 12.5, outline: 3, pupil: 5.8, glint: 1.9, ring: 6, mpupil: 6.4, gap: 6 },
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
const poly = (points: readonly (readonly [number, number])[]) => `M${points.map(([x, y]) => `${x} ${y}`).join(" L")} Z`;
|
|
41
|
+
|
|
42
|
+
function plate(cy: number) {
|
|
43
|
+
return {
|
|
44
|
+
top: poly([[100, cy - H], [100 + W, cy], [100, cy + H], [100 - W, cy]]),
|
|
45
|
+
left: poly([[100 - W, cy], [100, cy + H], [100, cy + H + T], [100 - W, cy + T]]),
|
|
46
|
+
right: poly([[100, cy + H], [100 + W, cy], [100 + W, cy + T], [100, cy + H + T]]),
|
|
47
|
+
outline: poly([[100 - W, cy], [100, cy - H], [100 + W, cy], [100 + W, cy + T], [100, cy + H + T], [100 - W, cy + T]]),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const node = (tag: string, attrs: SvgNode["attrs"], children?: readonly SvgNode[], style?: SvgNode["style"]): SvgNode =>
|
|
52
|
+
({ tag, attrs, ...(style ? { style } : {}), ...(children ? { children } : {}) });
|
|
53
|
+
const solid = (d: string, fill: string): SvgNode => node("path", { d }, undefined, { fill, stroke: fill, strokeWidth: 3, strokeLinejoin: "round" });
|
|
54
|
+
|
|
55
|
+
/** The avatar's `<svg>`. */
|
|
56
|
+
export function agentAvatar({ size = 56, variant = "color", color, label, className, idPrefix }: AgentAvatarOptions): SvgNode {
|
|
57
|
+
const id = idPrefix.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
58
|
+
const small = size <= 32;
|
|
59
|
+
const k = small ? CUTS.small : CUTS.full;
|
|
60
|
+
const bottom = plate(BOT), middle = plate(MID), lid = plate(k.lid);
|
|
61
|
+
const a11y: Record<string, string> = label ? { role: "img", "aria-label": label } : { "aria-hidden": "true" };
|
|
62
|
+
const svg = {
|
|
63
|
+
class: `fdn-agent-avatar${className ? ` ${className}` : ""}`,
|
|
64
|
+
width: size,
|
|
65
|
+
height: size,
|
|
66
|
+
viewBox: k.view,
|
|
67
|
+
focusable: "false",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (variant === "mono") {
|
|
71
|
+
const full = node("rect", { x: "0", y: "0", width: "200", height: "200", fill: "#fff" });
|
|
72
|
+
const grow = { fill: "#000", stroke: "#000", "stroke-width": k.gap * 2, "stroke-linejoin": "round" };
|
|
73
|
+
const erx = k.rx + k.ring / 2 + k.gap, ery = k.ry + k.ring / 2 + k.gap;
|
|
74
|
+
const mask = (suffix: string, children: readonly SvgNode[]) =>
|
|
75
|
+
node("mask", { id: `${id}${suffix}`, maskUnits: "userSpaceOnUse", x: "0", y: "0", width: "200", height: "200" }, [full, ...children]);
|
|
76
|
+
return node("svg", { ...svg, ...a11y }, [
|
|
77
|
+
node("defs", {}, [
|
|
78
|
+
mask("b", [node("path", { d: middle.outline, ...grow }),
|
|
79
|
+
...(small ? [] : [node("path", { d: TAIL, fill: "none", stroke: "#000", "stroke-width": 6 + k.gap * 2, "stroke-linecap": "round" })])]),
|
|
80
|
+
mask("m", [node("path", { d: lid.outline, ...grow }),
|
|
81
|
+
...k.eyes.map((x) => node("ellipse", { cx: x, cy: EYE_Y, rx: erx, ry: ery, fill: "#000" }))]),
|
|
82
|
+
mask("e", [node("path", { d: lid.outline, ...grow })]),
|
|
83
|
+
]),
|
|
84
|
+
node("g", { fill: "currentColor", stroke: "currentColor", "stroke-linejoin": "round" }, [
|
|
85
|
+
node("path", { d: bottom.outline, "stroke-width": 3, mask: `url(#${id}b)` }),
|
|
86
|
+
...(small ? [] : [node("path", { d: TAIL, fill: "none", "stroke-width": 6, "stroke-linecap": "round" })]),
|
|
87
|
+
node("path", { d: middle.outline, "stroke-width": 3, mask: `url(#${id}m)` }),
|
|
88
|
+
node("g", { mask: `url(#${id}e)` }, k.eyes.map((x) => node("g", {}, [
|
|
89
|
+
node("ellipse", { cx: x, cy: EYE_Y, rx: k.rx, ry: k.ry, fill: "none", "stroke-width": k.ring }),
|
|
90
|
+
node("circle", { cx: x + 1, cy: EYE_Y - 1.5, r: k.mpupil, stroke: "none" }),
|
|
91
|
+
]))),
|
|
92
|
+
node("path", { d: lid.outline, "stroke-width": 3 }),
|
|
93
|
+
]),
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const style = color ? { "--fdn-agent-thread": color, "--fdn-agent-thread-side": `color-mix(in srgb, ${color} 72%, #000)` } : undefined;
|
|
98
|
+
const plateSide = "var(--fdn-agent-plate-side, #0A0E1A)", plateTop = "var(--fdn-agent-plate, #1C2436)";
|
|
99
|
+
const threadSide = "var(--fdn-agent-thread-side, #A8143E)", thread = "var(--fdn-agent-thread, #E2325A)";
|
|
100
|
+
return node("svg", { ...svg, ...a11y }, [
|
|
101
|
+
solid(bottom.left, plateSide),
|
|
102
|
+
solid(bottom.right, plateSide),
|
|
103
|
+
solid(bottom.top, plateTop),
|
|
104
|
+
...(small ? [] : [node("path", { d: TAIL, fill: "none" }, undefined, { stroke: thread, strokeWidth: 6, strokeLinecap: "round" })]),
|
|
105
|
+
solid(middle.left, threadSide),
|
|
106
|
+
solid(middle.right, threadSide),
|
|
107
|
+
solid(middle.top, thread),
|
|
108
|
+
...(small ? [] : [node("ellipse", { cx: "100", cy: "110", rx: "34", ry: "11" }, undefined, { fill: "var(--fdn-agent-hollow, rgba(10, 14, 26, 0.38))" })]),
|
|
109
|
+
...k.eyes.map((x) => node("g", {}, [
|
|
110
|
+
node("ellipse", { cx: x, cy: EYE_Y, rx: k.rx, ry: k.ry }, undefined,
|
|
111
|
+
{ fill: "var(--fdn-agent-eye, #FFFFFF)", stroke: "var(--fdn-agent-pupil, #0A0E1A)", strokeWidth: k.outline }),
|
|
112
|
+
node("circle", { cx: x + 1, cy: EYE_Y - 1.5, r: k.pupil }, undefined, { fill: "var(--fdn-agent-pupil, #0A0E1A)" }),
|
|
113
|
+
node("circle", { cx: x + 2.6, cy: EYE_Y - 1.5 - k.pupil * 0.45, r: k.glint }, undefined, { fill: "var(--fdn-agent-eye, #FFFFFF)" }),
|
|
114
|
+
])),
|
|
115
|
+
solid(lid.left, plateSide),
|
|
116
|
+
solid(lid.right, plateSide),
|
|
117
|
+
solid(lid.top, plateTop),
|
|
118
|
+
], style);
|
|
119
|
+
}
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** The panel's browser behaviours, for any framework: a skin calls them from its own lifecycle. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeps a scroll box at the bottom while its content (its first child) grows -- unless the user scrolled up to
|
|
5
|
+
* read; then it stays where they are until they scroll back down. Call `stop` when the box goes.
|
|
6
|
+
*/
|
|
7
|
+
export function stickToBottom(box: HTMLElement, threshold = 48): { readonly stuck: boolean; stop(): void } {
|
|
8
|
+
let stuck = true;
|
|
9
|
+
const onScroll = () => {
|
|
10
|
+
stuck = box.scrollHeight - box.scrollTop - box.clientHeight < threshold;
|
|
11
|
+
};
|
|
12
|
+
box.addEventListener("scroll", onScroll, { passive: true });
|
|
13
|
+
const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => {
|
|
14
|
+
if (stuck) box.scrollTop = box.scrollHeight;
|
|
15
|
+
});
|
|
16
|
+
const content = box.firstElementChild;
|
|
17
|
+
if (observer && content) observer.observe(content);
|
|
18
|
+
return {
|
|
19
|
+
get stuck() {
|
|
20
|
+
return stuck;
|
|
21
|
+
},
|
|
22
|
+
stop() {
|
|
23
|
+
box.removeEventListener("scroll", onScroll);
|
|
24
|
+
observer?.disconnect();
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Calls `close` on a press outside `anchor` (a menu and its button) or on Escape. Returns the stop. */
|
|
30
|
+
export function dismissOnOutside(anchor: HTMLElement, close: () => void): () => void {
|
|
31
|
+
const onPointer = (e: PointerEvent) => {
|
|
32
|
+
if (!anchor.contains(e.target as Node)) close();
|
|
33
|
+
};
|
|
34
|
+
const onKey = (e: KeyboardEvent) => {
|
|
35
|
+
if (e.key === "Escape") close();
|
|
36
|
+
};
|
|
37
|
+
anchor.ownerDocument.addEventListener("pointerdown", onPointer);
|
|
38
|
+
anchor.ownerDocument.addEventListener("keydown", onKey);
|
|
39
|
+
return () => {
|
|
40
|
+
anchor.ownerDocument.removeEventListener("pointerdown", onPointer);
|
|
41
|
+
anchor.ownerDocument.removeEventListener("keydown", onKey);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Grows a text box with its text, up to a third of the window (at least 120 px). */
|
|
46
|
+
export function fitToContent(box: HTMLTextAreaElement): void {
|
|
47
|
+
const view = box.ownerDocument.defaultView;
|
|
48
|
+
box.style.height = "auto";
|
|
49
|
+
box.style.height = `${Math.min(box.scrollHeight, Math.max(120, (view?.innerHeight ?? 360) / 3))}px`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Focuses a text box with the caret after its text (after "Edit", or the host attached something). */
|
|
53
|
+
export function focusAtEnd(box: HTMLTextAreaElement): void {
|
|
54
|
+
box.focus();
|
|
55
|
+
const end = box.value.length;
|
|
56
|
+
box.setSelectionRange(end, end);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What a key in the composer does (`composerKey`). */
|
|
60
|
+
export type ComposerAction = "send" | "stop" | "pick" | "next" | "previous" | "close";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What a key in the composer does: Enter sends (Shift+Enter is a new line, and an input method that is still
|
|
64
|
+
* composing keeps its Enter), Escape stops the running answer. While the skill picker is open (`pickerOpen`), the
|
|
65
|
+
* arrows move in it, Enter and Tab pick, and Escape closes it.
|
|
66
|
+
*/
|
|
67
|
+
export function composerKey(e: Pick<KeyboardEvent, "key" | "shiftKey" | "isComposing">, busy: boolean, pickerOpen = false): ComposerAction | undefined {
|
|
68
|
+
if (pickerOpen && !e.isComposing) {
|
|
69
|
+
if (e.key === "ArrowDown") return "next";
|
|
70
|
+
if (e.key === "ArrowUp") return "previous";
|
|
71
|
+
if ((e.key === "Enter" && !e.shiftKey) || e.key === "Tab") return "pick";
|
|
72
|
+
if (e.key === "Escape") return "close";
|
|
73
|
+
}
|
|
74
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) return "send";
|
|
75
|
+
if (e.key === "Escape" && busy) return "stop";
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The pictures among pasted or dropped files. */
|
|
80
|
+
export function picturesIn(transfer: DataTransfer | null | undefined): File[] {
|
|
81
|
+
return Array.from(transfer?.files ?? []).filter((f) => f.type.startsWith("image/"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A picture made ready for a message: a `data:` URL, and its size in pixels. */
|
|
85
|
+
export interface ReadPicture {
|
|
86
|
+
dataUrl: string;
|
|
87
|
+
width: number;
|
|
88
|
+
height: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A picture as a message takes it: at most `maxSide` pixels on its longer side (models scale larger ones down
|
|
93
|
+
* anyway, and the transcript keeps it), as JPEG on white (transparent parts of a screenshot stay readable).
|
|
94
|
+
* Throws when the browser cannot read it as a picture.
|
|
95
|
+
*/
|
|
96
|
+
export async function readPicture(picture: Blob, options: { maxSide?: number; quality?: number } = {}): Promise<ReadPicture> {
|
|
97
|
+
const maxSide = options.maxSide ?? 1600;
|
|
98
|
+
const bitmap = await createImageBitmap(picture);
|
|
99
|
+
try {
|
|
100
|
+
const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height, 1));
|
|
101
|
+
const width = Math.max(1, Math.round(bitmap.width * scale));
|
|
102
|
+
const height = Math.max(1, Math.round(bitmap.height * scale));
|
|
103
|
+
const canvas = document.createElement("canvas");
|
|
104
|
+
canvas.width = width;
|
|
105
|
+
canvas.height = height;
|
|
106
|
+
const context = canvas.getContext("2d");
|
|
107
|
+
if (!context) throw new Error("No canvas to draw the picture on.");
|
|
108
|
+
context.fillStyle = "#fff";
|
|
109
|
+
context.fillRect(0, 0, width, height);
|
|
110
|
+
context.drawImage(bitmap, 0, 0, width, height);
|
|
111
|
+
return { dataUrl: canvas.toDataURL("image/jpeg", options.quality ?? 0.85), width, height };
|
|
112
|
+
} finally {
|
|
113
|
+
bitmap.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** How long a copy button shows its tick. */
|
|
118
|
+
export const COPIED_MS = 1500;
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chat's icons as SVG path data on a 24 x 24 grid, drawn in `currentColor`: every skin draws the same ones.
|
|
3
|
+
*
|
|
4
|
+
* Most paths are Google's Material Icons (https://github.com/google/material-design-icons), some of them changed,
|
|
5
|
+
* under the Apache License 2.0. The package's NOTICE says so.
|
|
6
|
+
*/
|
|
7
|
+
export const iconPaths = {
|
|
8
|
+
send: "M3.4 20.4 21 12 3.4 3.6l-.01 6.53L15 12 3.39 13.87z",
|
|
9
|
+
image: "M21 19V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5z",
|
|
10
|
+
camera: "M12 15.2a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4zM9 2 7.17 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-3.17L15 2z",
|
|
11
|
+
translate: "m12.87 15.07-2.54-2.51.03-.03A17.52 17.52 0 0 0 14.07 6H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2zm-2.62 7 1.62-4.33L19.12 17z",
|
|
12
|
+
stop: "M7 7h10v10H7z",
|
|
13
|
+
plus: "M11 5h2v6h6v2h-6v6h-2v-6H5v-2h6z",
|
|
14
|
+
history: "M13 3a9 9 0 0 0-9 9H1l3.9 3.9.1.1L9 12H6a7 7 0 1 1 2.05 4.95l-1.42 1.42A9 9 0 1 0 13 3zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8z",
|
|
15
|
+
chevron: "M7.4 8.6 12 13.2l4.6-4.6L18 10l-6 6-6-6z",
|
|
16
|
+
copy: "M16 1H4a2 2 0 0 0-2 2v14h2V3h12zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H8V7h11z",
|
|
17
|
+
check: "M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z",
|
|
18
|
+
close: "M19 6.4 17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z",
|
|
19
|
+
tool: "M22.7 19 13.6 9.9c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z",
|
|
20
|
+
warning: "M1 21h22L12 2zm12-3h-2v-2h2zm0-4h-2v-4h2z",
|
|
21
|
+
shield: "M12 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11z",
|
|
22
|
+
info: "M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z",
|
|
23
|
+
/** After a link that opens in a new tab. */
|
|
24
|
+
external: "M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5z",
|
|
25
|
+
edit: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75z",
|
|
26
|
+
retry: "M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4z",
|
|
27
|
+
trash: "M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6zM19 4h-3.5l-1-1h-5l-1 1H5v2h14z",
|
|
28
|
+
sparkle: "M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4z",
|
|
29
|
+
page: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zm4 18H6V4h7v5h5z",
|
|
30
|
+
brain: "M12 3a4 4 0 0 0-4 4v.1A3.5 3.5 0 0 0 5 10.5c0 .9.3 1.7.9 2.4A3.5 3.5 0 0 0 8 19h.2A4 4 0 0 0 16 19h.2a3.5 3.5 0 0 0 2-6.1c.5-.7.8-1.5.8-2.4A3.5 3.5 0 0 0 16 7.1V7a4 4 0 0 0-4-4zm-1 3v12H9a1.5 1.5 0 0 1 0-3V9A1.5 1.5 0 0 1 11 6zm2 0a1.5 1.5 0 0 1 2 3v6a1.5 1.5 0 0 1 0 3h-2z",
|
|
31
|
+
quote: "M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z",
|
|
32
|
+
book: "M18 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zM6 4h5v8l-2.5-1.5L6 12z",
|
|
33
|
+
attach: "M16.5 6v11.5a4 4 0 0 1-8 0V5a2.5 2.5 0 0 1 5 0v10.5a1 1 0 0 1-2 0V6H10v9.5a2.5 2.5 0 0 0 5 0V5a4 4 0 0 0-8 0v12.5a5.5 5.5 0 0 0 11 0V6z",
|
|
34
|
+
tabs: "M21 3H3a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 16H3V5h10v4h8z",
|
|
35
|
+
settings: "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.48.48 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.48.48 0 0 0-.59.22L2.74 8.87a.47.47 0 0 0 .12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32a.47.47 0 0 0-.12-.61zM12 15.6A3.6 3.6 0 1 1 12 8.4a3.6 3.6 0 0 1 0 7.2z",
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
export type IconName = keyof typeof iconPaths;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { iconPaths, type IconName } from "./icons.js";
|
|
2
|
+
export { agentAvatar, type AgentAvatarOptions, type SvgNode } from "./avatar.js";
|
|
3
|
+
export {
|
|
4
|
+
COPIED_MS,
|
|
5
|
+
composerKey,
|
|
6
|
+
dismissOnOutside,
|
|
7
|
+
fitToContent,
|
|
8
|
+
focusAtEnd,
|
|
9
|
+
picturesIn,
|
|
10
|
+
readPicture,
|
|
11
|
+
stickToBottom,
|
|
12
|
+
type ComposerAction,
|
|
13
|
+
type ReadPicture,
|
|
14
|
+
} from "./dom.js";
|
|
15
|
+
export {
|
|
16
|
+
approvalIcon,
|
|
17
|
+
diffMarker,
|
|
18
|
+
headingTag,
|
|
19
|
+
noticeIcon,
|
|
20
|
+
noticeRole,
|
|
21
|
+
toolBadges,
|
|
22
|
+
toolClassName,
|
|
23
|
+
toolStatusIcon,
|
|
24
|
+
turnMeta,
|
|
25
|
+
type Badge,
|
|
26
|
+
} from "./presentation.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ApprovalPart, DiffLine, NoticeSeverity, ToolCallPart, TurnViewModel, UiStrings } from "@fadenstack/ui";
|
|
2
|
+
|
|
3
|
+
import type { IconName } from "./icons.js";
|
|
4
|
+
|
|
5
|
+
/** How the parts look, decided once for every skin: classes, icons and badges the stylesheet expects. */
|
|
6
|
+
|
|
7
|
+
/** The tool line's classes: failed, or not run (denied, stopped). */
|
|
8
|
+
export function toolClassName(part: ToolCallPart): string {
|
|
9
|
+
const failed = part.state === "failed";
|
|
10
|
+
const denied = part.state === "denied" || part.state === "cancelled";
|
|
11
|
+
return `fdn-tool${failed ? " fdn-tool-failed" : ""}${denied ? " fdn-tool-denied" : ""}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The tool line's status icon; none while it runs (a skin shows its spinner). */
|
|
15
|
+
export function toolStatusIcon(part: ToolCallPart): IconName | undefined {
|
|
16
|
+
if (part.isRunning) return undefined;
|
|
17
|
+
if (part.state === "failed") return "warning";
|
|
18
|
+
return part.state === "denied" || part.state === "cancelled" ? "close" : "check";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A badge: its text and classes. */
|
|
22
|
+
export interface Badge {
|
|
23
|
+
text: string;
|
|
24
|
+
className: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The tool line's badges: where the tool runs when not here, and what a tool here does when it changes something. */
|
|
28
|
+
export function toolBadges(part: ToolCallPart, strings: UiStrings): Badge[] {
|
|
29
|
+
if (part.source !== "host") return [{ text: part.source === "gateway" ? strings.serverTool : part.source, className: "fdn-badge" }];
|
|
30
|
+
if (part.toolClass !== "read") return [{ text: part.classLabel, className: `fdn-badge fdn-class-${part.toolClass}` }];
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function approvalIcon(part: ApprovalPart): IconName {
|
|
35
|
+
return part.isDangerous ? "warning" : "shield";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function noticeIcon(severity: NoticeSeverity): IconName {
|
|
39
|
+
return severity === "info" ? "info" : severity === "blocked" ? "shield" : "warning";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A notice is read out at once unless it only informs. */
|
|
43
|
+
export function noticeRole(severity: NoticeSeverity): "note" | "alert" {
|
|
44
|
+
return severity === "info" ? "note" : "alert";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The mark before a diff line. */
|
|
48
|
+
export function diffMarker(kind: DiffLine["kind"]): string {
|
|
49
|
+
return kind === "added" ? "+ " : kind === "removed" ? "- " : " ";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** An answer's heading: two levels below the panel's own headings, h6 at most. */
|
|
53
|
+
export function headingTag(level: number): "h3" | "h4" | "h5" | "h6" {
|
|
54
|
+
return `h${Math.min(6, level + 2)}` as "h3" | "h4" | "h5" | "h6";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The line under a finished answer: how long it took and what it used. */
|
|
58
|
+
export function turnMeta(turn: TurnViewModel): string {
|
|
59
|
+
return [turn.durationText, turn.usageText].filter(Boolean).join(" · ");
|
|
60
|
+
}
|