@4djs/assistant 0.0.0

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.
Files changed (68) hide show
  1. package/README.md +322 -0
  2. package/package.json +41 -0
  3. package/src/core/chat-activity.ts +107 -0
  4. package/src/core/chat-commands.ts +173 -0
  5. package/src/core/chat-history.ts +113 -0
  6. package/src/core/chat-reply-suggestions-parse.ts +119 -0
  7. package/src/core/code-highlight.ts +20 -0
  8. package/src/core/create-assistant-store.ts +639 -0
  9. package/src/core/fetch-suggested-prompts.ts +53 -0
  10. package/src/core/index.ts +125 -0
  11. package/src/core/interactive-tools/choices.ts +155 -0
  12. package/src/core/interactive-tools/confirmation.ts +63 -0
  13. package/src/core/interactive-tools/constants.ts +22 -0
  14. package/src/core/interactive-tools/execute.ts +70 -0
  15. package/src/core/interactive-tools/index.ts +41 -0
  16. package/src/core/interactive-tools/suggestions.ts +87 -0
  17. package/src/core/interactive-tools/waiters.ts +55 -0
  18. package/src/core/llm-chat.ts +686 -0
  19. package/src/core/llm-config.ts +101 -0
  20. package/src/core/llm-models.ts +96 -0
  21. package/src/core/llm-provider.ts +99 -0
  22. package/src/core/llm-settings-storage.ts +331 -0
  23. package/src/core/llm-sse.ts +166 -0
  24. package/src/core/llm-types.ts +52 -0
  25. package/src/core/markdown-utils.ts +11 -0
  26. package/src/core/prepare-markdown.ts +38 -0
  27. package/src/core/types.ts +86 -0
  28. package/src/css.d.ts +1 -0
  29. package/src/react/Assistant.tsx +358 -0
  30. package/src/react/components/HighlightedJsonCode.tsx +24 -0
  31. package/src/react/components/MarkdownContent.tsx +98 -0
  32. package/src/react/components/MarkdownEditor.tsx +60 -0
  33. package/src/react/components/MermaidDiagram.tsx +139 -0
  34. package/src/react/components/ModelSelector.tsx +243 -0
  35. package/src/react/components/chat/AssistantErrorCallout.tsx +79 -0
  36. package/src/react/components/chat/ChatActivity.tsx +274 -0
  37. package/src/react/components/chat/ChatComposer.tsx +189 -0
  38. package/src/react/components/chat/ChatEmptyState.tsx +145 -0
  39. package/src/react/components/chat/ChatInteractivePrompt/choices-prompt.tsx +262 -0
  40. package/src/react/components/chat/ChatInteractivePrompt/confirmation-prompt.tsx +97 -0
  41. package/src/react/components/chat/ChatInteractivePrompt/index.tsx +60 -0
  42. package/src/react/components/chat/ChatInteractivePrompt/shell.tsx +60 -0
  43. package/src/react/components/chat/ChatInteractivePrompt/utils.ts +14 -0
  44. package/src/react/components/chat/ChatMessage.tsx +150 -0
  45. package/src/react/components/chat/ChatMessageScroll.tsx +116 -0
  46. package/src/react/components/chat/ChatReplySuggestions.tsx +231 -0
  47. package/src/react/components/chat/ComposerCommandMenu.tsx +69 -0
  48. package/src/react/components/chat/LlmSettingsStrip.tsx +348 -0
  49. package/src/react/components/chat/LlmSetupPrompt.tsx +58 -0
  50. package/src/react/components/chat/LlmUnavailableBanner.tsx +11 -0
  51. package/src/react/components/chat/SuggestedPromptsList.tsx +121 -0
  52. package/src/react/components/chat/SuggestedPromptsStrip.tsx +72 -0
  53. package/src/react/components/chat/SystemPromptField.tsx +107 -0
  54. package/src/react/components/highlighted-code.tsx +107 -0
  55. package/src/react/context.tsx +72 -0
  56. package/src/react/hooks/use-composer-commands.ts +129 -0
  57. package/src/react/hooks/use-suggested-prompts.ts +128 -0
  58. package/src/react/index.ts +39 -0
  59. package/src/react/lib/parse-assistant-error.ts +96 -0
  60. package/src/react/lib/prompt-icons.ts +40 -0
  61. package/src/react/types.ts +83 -0
  62. package/src/react/utils/cn.ts +5 -0
  63. package/src/styles/assistant.css +3009 -0
  64. package/test/buildLlmHistory.test.ts +95 -0
  65. package/test/llm-config.test.ts +72 -0
  66. package/test/llmSettingsStorage.test.ts +121 -0
  67. package/test/parse-assistant-error.test.ts +24 -0
  68. package/tsconfig.json +8 -0
@@ -0,0 +1,113 @@
1
+ import type { ChatActivityStep } from "./chat-activity.ts";
2
+ import type { ReplySuggestions } from "./interactive-tools/index.ts";
3
+ import { isValidReplySuggestions } from "./interactive-tools/index.ts";
4
+ import type { AssistantMessage, AssistantWelcomeContext } from "./types.ts";
5
+
6
+ const DEFAULT_HISTORY_KEY = "assistant-chat-history";
7
+ const DEFAULT_MAX_STORED = 100;
8
+
9
+ export interface ChatHistoryOptions {
10
+ storageKey?: string;
11
+ maxStored?: number;
12
+ welcomeMessage: (ctx: AssistantWelcomeContext) => AssistantMessage;
13
+ }
14
+
15
+ interface StoredChatMessage {
16
+ id: string;
17
+ role: "user" | "assistant" | "system";
18
+ content: string;
19
+ activity?: ChatActivityStep[];
20
+ toolCall?: { name: string; args: Record<string, unknown> };
21
+ replySuggestions?: ReplySuggestions;
22
+ isError?: boolean;
23
+ llmSetupRequired?: boolean;
24
+ timestamp: number;
25
+ }
26
+
27
+ function toStored(message: AssistantMessage): StoredChatMessage {
28
+ return {
29
+ id: message.id,
30
+ role: message.role,
31
+ content: message.content,
32
+ activity: message.activity,
33
+ toolCall: message.toolCall,
34
+ replySuggestions: message.replySuggestions,
35
+ isError: message.isError,
36
+ llmSetupRequired: message.llmSetupRequired,
37
+ timestamp: message.timestamp,
38
+ };
39
+ }
40
+
41
+ function fromStored(message: StoredChatMessage): AssistantMessage {
42
+ return {
43
+ id: message.id,
44
+ role: message.role,
45
+ content: message.content,
46
+ activity: message.activity,
47
+ toolCall: message.toolCall,
48
+ replySuggestions: isValidReplySuggestions(message.replySuggestions)
49
+ ? message.replySuggestions
50
+ : undefined,
51
+ isError: message.isError,
52
+ llmSetupRequired: message.llmSetupRequired,
53
+ timestamp: message.timestamp,
54
+ };
55
+ }
56
+
57
+ function isStoredChatMessage(value: unknown): value is StoredChatMessage {
58
+ if (!value || typeof value !== "object") return false;
59
+ const message = value as StoredChatMessage;
60
+ return (
61
+ typeof message.id === "string" &&
62
+ (message.role === "user" ||
63
+ message.role === "assistant" ||
64
+ message.role === "system") &&
65
+ typeof message.content === "string" &&
66
+ typeof message.timestamp === "number"
67
+ );
68
+ }
69
+
70
+ export function createChatHistoryHelpers(options: ChatHistoryOptions) {
71
+ const storageKey = options.storageKey ?? DEFAULT_HISTORY_KEY;
72
+ const maxStored = options.maxStored ?? DEFAULT_MAX_STORED;
73
+
74
+ function getStored(): AssistantMessage[] | null {
75
+ try {
76
+ const raw = localStorage.getItem(storageKey);
77
+ if (!raw) return null;
78
+ const parsed: unknown = JSON.parse(raw);
79
+ if (!Array.isArray(parsed)) return null;
80
+ const messages = parsed.filter(isStoredChatMessage).map(fromStored);
81
+ return messages.length > 0 ? messages : null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function persist(messages: AssistantMessage[]) {
88
+ const storable = messages
89
+ .filter((message) => !message.streaming)
90
+ .slice(-maxStored)
91
+ .map(toStored);
92
+ localStorage.setItem(storageKey, JSON.stringify(storable));
93
+ }
94
+
95
+ function clear() {
96
+ localStorage.removeItem(storageKey);
97
+ }
98
+
99
+ function loadInitial(ctx: AssistantWelcomeContext): AssistantMessage[] {
100
+ const stored = getStored();
101
+ const welcome = options.welcomeMessage(ctx);
102
+ if (stored) {
103
+ const hasWelcome = stored.some((message) => message.id === "welcome");
104
+ if (hasWelcome) return stored;
105
+ return [welcome, ...stored];
106
+ }
107
+ return [welcome];
108
+ }
109
+
110
+ return { getStored, persist, clear, loadInitial };
111
+ }
112
+
113
+ export type { AssistantMessage };
@@ -0,0 +1,119 @@
1
+ export type SuggestionField = {
2
+ label: string;
3
+ value: string;
4
+ };
5
+
6
+ export type ParsedSuggestion =
7
+ | { kind: "compact"; text: string }
8
+ | {
9
+ kind: "structured";
10
+ title: string;
11
+ description?: string;
12
+ fields: SuggestionField[];
13
+ raw: string;
14
+ }
15
+ | { kind: "custom"; text: string };
16
+
17
+ const STRUCTURED_FIELD_LABELS = new Set([
18
+ "name",
19
+ "description",
20
+ "model",
21
+ "enabled",
22
+ "tools",
23
+ "prompt",
24
+ "agent",
25
+ "dataclass",
26
+ "filter",
27
+ ]);
28
+
29
+ export function parseSuggestionText(text: string): ParsedSuggestion {
30
+ const trimmed = text.trim();
31
+ if (!trimmed) {
32
+ return { kind: "compact", text: trimmed };
33
+ }
34
+
35
+ if (isCustomDeclineSuggestion(trimmed)) {
36
+ return { kind: "custom", text: trimmed };
37
+ }
38
+
39
+ const fields = parseKeyValueFields(trimmed);
40
+ if (fields && fields.length >= 2) {
41
+ const nameField = fields.find(
42
+ (field) => field.label.toLowerCase() === "name",
43
+ );
44
+ const descriptionField = fields.find(
45
+ (field) => field.label.toLowerCase() === "description",
46
+ );
47
+ const detailFields = fields.filter(
48
+ (field) => !["name", "description"].includes(field.label.toLowerCase()),
49
+ );
50
+
51
+ return {
52
+ kind: "structured",
53
+ title: nameField?.value ?? trimmed,
54
+ description: descriptionField?.value,
55
+ fields: detailFields,
56
+ raw: trimmed,
57
+ };
58
+ }
59
+
60
+ if (trimmed.length <= 32 && !trimmed.includes(",")) {
61
+ return { kind: "compact", text: trimmed };
62
+ }
63
+
64
+ return { kind: "compact", text: trimmed };
65
+ }
66
+
67
+ function isCustomDeclineSuggestion(text: string): boolean {
68
+ return /\b(myself|my own|custom answer|provide the details|type my own|enter manually)\b/i.test(
69
+ text,
70
+ );
71
+ }
72
+
73
+ function parseKeyValueFields(text: string): SuggestionField[] | null {
74
+ const pattern = /([A-Za-z][A-Za-z\s]*?):\s*/g;
75
+ const matches = [...text.matchAll(pattern)];
76
+
77
+ if (matches.length < 2) {
78
+ return null;
79
+ }
80
+
81
+ const fields: SuggestionField[] = [];
82
+
83
+ for (let index = 0; index < matches.length; index += 1) {
84
+ const match = matches[index];
85
+ if (!match || match.index === undefined) {
86
+ continue;
87
+ }
88
+
89
+ const label = match[1]?.trim() ?? "";
90
+ if (!STRUCTURED_FIELD_LABELS.has(label.toLowerCase())) {
91
+ return null;
92
+ }
93
+
94
+ const valueStart = match.index + match[0].length;
95
+ const nextMatch = matches[index + 1];
96
+ const valueEnd = nextMatch?.index ?? text.length;
97
+ const value = text.slice(valueStart, valueEnd).replace(/,\s*$/, "").trim();
98
+
99
+ if (!label || !value) {
100
+ return null;
101
+ }
102
+
103
+ fields.push({ label, value });
104
+ }
105
+
106
+ return fields.length >= 2 ? fields : null;
107
+ }
108
+
109
+ export function splitToolValues(value: string): string[] {
110
+ const normalized = value.trim();
111
+ if (!normalized || /^none$/i.test(normalized)) {
112
+ return [];
113
+ }
114
+
115
+ return normalized
116
+ .split(/,\s*/)
117
+ .map((entry) => entry.trim())
118
+ .filter(Boolean);
119
+ }
@@ -0,0 +1,20 @@
1
+ import hljs from "highlight.js/lib/core";
2
+ import jsonLang from "highlight.js/lib/languages/json";
3
+ import markdownLang from "highlight.js/lib/languages/markdown";
4
+
5
+ hljs.registerLanguage("json", jsonLang);
6
+ hljs.registerLanguage("markdown", markdownLang);
7
+
8
+ export function normalizeCodeLineEndings(code: string): string {
9
+ return code.replace(/\r\n?/g, "\n");
10
+ }
11
+
12
+ export function highlightCode(code: string, language: string | null): string {
13
+ const source = normalizeCodeLineEndings(code);
14
+
15
+ if (language && hljs.getLanguage(language)) {
16
+ return hljs.highlight(source, { language, ignoreIllegals: true }).value;
17
+ }
18
+
19
+ return hljs.highlightAuto(source).value;
20
+ }