@slices.sh/ai-chat 0.1.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.
- package/LICENSE +21 -0
- package/dist/exports.json +794 -0
- package/dist/fixture/fixture-approval.d.ts +4 -0
- package/dist/fixture/fixture-approval.js +59 -0
- package/dist/fixture/fixture-artifacts.d.ts +2 -0
- package/dist/fixture/fixture-artifacts.js +48 -0
- package/dist/fixture/fixture-question.d.ts +5 -0
- package/dist/fixture/fixture-question.js +93 -0
- package/dist/fixture/fixture-script.d.ts +35 -0
- package/dist/fixture/fixture-script.js +280 -0
- package/dist/fixture/fixture-selection.d.ts +5 -0
- package/dist/fixture/fixture-selection.js +20 -0
- package/dist/fixture/fixture.d.ts +20 -0
- package/dist/fixture/fixture.js +75 -0
- package/dist/fixture/provider.d.ts +15 -0
- package/dist/fixture/provider.js +14 -0
- package/dist/headless/always-allow.d.ts +2 -0
- package/dist/headless/always-allow.js +24 -0
- package/dist/headless/answer.d.ts +6 -0
- package/dist/headless/answer.js +29 -0
- package/dist/headless/approval-error.d.ts +2 -0
- package/dist/headless/approval-error.js +16 -0
- package/dist/headless/approval-reason.d.ts +2 -0
- package/dist/headless/approval-reason.js +13 -0
- package/dist/headless/approval-receipt.d.ts +4 -0
- package/dist/headless/approval-receipt.js +13 -0
- package/dist/headless/approve.d.ts +4 -0
- package/dist/headless/approve.js +15 -0
- package/dist/headless/branch-navigation.d.ts +2 -0
- package/dist/headless/branch-navigation.js +11 -0
- package/dist/headless/branch-next.d.ts +2 -0
- package/dist/headless/branch-next.js +21 -0
- package/dist/headless/branch-page.d.ts +5 -0
- package/dist/headless/branch-page.js +13 -0
- package/dist/headless/branch-previous.d.ts +2 -0
- package/dist/headless/branch-previous.js +21 -0
- package/dist/headless/cancel-edit.d.ts +2 -0
- package/dist/headless/cancel-edit.js +10 -0
- package/dist/headless/cancel-queue-edit.d.ts +2 -0
- package/dist/headless/cancel-queue-edit.js +15 -0
- package/dist/headless/cancel-queued.d.ts +2 -0
- package/dist/headless/cancel-queued.js +15 -0
- package/dist/headless/chat-dictation/confirm.d.ts +2 -0
- package/dist/headless/chat-dictation/confirm.js +14 -0
- package/dist/headless/chat-dictation/index.d.ts +12 -0
- package/dist/headless/chat-dictation/index.js +9 -0
- package/dist/headless/chat-dictation/input.d.ts +3 -0
- package/dist/headless/chat-dictation/input.js +42 -0
- package/dist/headless/chat-dictation/root.d.ts +3 -0
- package/dist/headless/chat-dictation/root.js +42 -0
- package/dist/headless/chat-dictation/text.d.ts +3 -0
- package/dist/headless/chat-dictation/text.js +14 -0
- package/dist/headless/chat-dictation/voice.d.ts +3 -0
- package/dist/headless/chat-dictation/voice.js +35 -0
- package/dist/headless/citation-content.d.ts +3 -0
- package/dist/headless/citation-content.js +19 -0
- package/dist/headless/citation-description.d.ts +2 -0
- package/dist/headless/citation-description.js +12 -0
- package/dist/headless/citation-root.d.ts +2 -0
- package/dist/headless/citation-root.js +13 -0
- package/dist/headless/citation-title.d.ts +2 -0
- package/dist/headless/citation-title.js +12 -0
- package/dist/headless/citation-trigger.d.ts +2 -0
- package/dist/headless/citation-trigger.js +12 -0
- package/dist/headless/clear-answer.d.ts +5 -0
- package/dist/headless/clear-answer.js +19 -0
- package/dist/headless/composer-root.d.ts +14 -0
- package/dist/headless/composer-root.js +12 -0
- package/dist/headless/composer-standalone.d.ts +3 -0
- package/dist/headless/composer-standalone.js +10 -0
- package/dist/headless/composer.d.ts +3 -0
- package/dist/headless/composer.js +24 -0
- package/dist/headless/context-usage.d.ts +3 -0
- package/dist/headless/context-usage.js +13 -0
- package/dist/headless/contexts.d.ts +77 -0
- package/dist/headless/contexts.js +53 -0
- package/dist/headless/continue.d.ts +4 -0
- package/dist/headless/continue.js +26 -0
- package/dist/headless/copy.d.ts +11 -0
- package/dist/headless/copy.js +21 -0
- package/dist/headless/deny.d.ts +4 -0
- package/dist/headless/deny.js +15 -0
- package/dist/headless/edit-queued.d.ts +2 -0
- package/dist/headless/edit-queued.js +15 -0
- package/dist/headless/edit.d.ts +2 -0
- package/dist/headless/edit.js +17 -0
- package/dist/headless/edited-attachments.d.ts +5 -0
- package/dist/headless/edited-attachments.js +11 -0
- package/dist/headless/edited-input.d.ts +2 -0
- package/dist/headless/edited-input.js +28 -0
- package/dist/headless/empty.d.ts +3 -0
- package/dist/headless/empty.js +12 -0
- package/dist/headless/failed.d.ts +15 -0
- package/dist/headless/failed.js +17 -0
- package/dist/headless/feedback-error.d.ts +2 -0
- package/dist/headless/feedback-error.js +11 -0
- package/dist/headless/feedback-root.d.ts +2 -0
- package/dist/headless/feedback-root.js +24 -0
- package/dist/headless/feedback.d.ts +4 -0
- package/dist/headless/feedback.js +18 -0
- package/dist/headless/file-audio.d.ts +2 -0
- package/dist/headless/file-audio.js +16 -0
- package/dist/headless/file-context.d.ts +4 -0
- package/dist/headless/file-context.js +7 -0
- package/dist/headless/file-img.d.ts +2 -0
- package/dist/headless/file-img.js +21 -0
- package/dist/headless/file-kind-gate.d.ts +5 -0
- package/dist/headless/file-kind-gate.js +11 -0
- package/dist/headless/file-link.d.ts +3 -0
- package/dist/headless/file-link.js +14 -0
- package/dist/headless/file-root.d.ts +5 -0
- package/dist/headless/file-root.js +13 -0
- package/dist/headless/file-unavailable.d.ts +2 -0
- package/dist/headless/file-unavailable.js +12 -0
- package/dist/headless/file-video.d.ts +2 -0
- package/dist/headless/file-video.js +16 -0
- package/dist/headless/follow-ups.d.ts +5 -0
- package/dist/headless/follow-ups.js +20 -0
- package/dist/headless/forget-approval.d.ts +2 -0
- package/dist/headless/forget-approval.js +21 -0
- package/dist/headless/gate-content.d.ts +2 -0
- package/dist/headless/gate-content.js +6 -0
- package/dist/headless/index.d.ts +249 -0
- package/dist/headless/index.js +251 -0
- package/dist/headless/input.d.ts +11 -0
- package/dist/headless/input.js +18 -0
- package/dist/headless/jump-to-latest.d.ts +3 -0
- package/dist/headless/jump-to-latest.js +14 -0
- package/dist/headless/message-edit-root.d.ts +2 -0
- package/dist/headless/message-edit-root.js +13 -0
- package/dist/headless/message-editing.d.ts +2 -0
- package/dist/headless/message-editing.js +10 -0
- package/dist/headless/message-editor.d.ts +2 -0
- package/dist/headless/message-editor.js +19 -0
- package/dist/headless/message-footer.d.ts +3 -0
- package/dist/headless/message-footer.js +15 -0
- package/dist/headless/message-reading.d.ts +2 -0
- package/dist/headless/message-reading.js +10 -0
- package/dist/headless/message.d.ts +11 -0
- package/dist/headless/message.js +34 -0
- package/dist/headless/messages.d.ts +9 -0
- package/dist/headless/messages.js +19 -0
- package/dist/headless/model-picker.d.ts +2 -0
- package/dist/headless/model-picker.js +14 -0
- package/dist/headless/model.d.ts +4 -0
- package/dist/headless/model.js +13 -0
- package/dist/headless/models.d.ts +5 -0
- package/dist/headless/models.js +11 -0
- package/dist/headless/optional-tools.d.ts +7 -0
- package/dist/headless/optional-tools.js +12 -0
- package/dist/headless/other-answer.d.ts +5 -0
- package/dist/headless/other-answer.js +32 -0
- package/dist/headless/part-approval.d.ts +2 -0
- package/dist/headless/part-approval.js +33 -0
- package/dist/headless/part-file.d.ts +6 -0
- package/dist/headless/part-file.js +11 -0
- package/dist/headless/part-question.d.ts +2 -0
- package/dist/headless/part-question.js +57 -0
- package/dist/headless/part-reasoning.d.ts +15 -0
- package/dist/headless/part-reasoning.js +26 -0
- package/dist/headless/part-source.d.ts +5 -0
- package/dist/headless/part-source.js +20 -0
- package/dist/headless/part-text.d.ts +16 -0
- package/dist/headless/part-text.js +29 -0
- package/dist/headless/part-tool.d.ts +16 -0
- package/dist/headless/part-tool.js +14 -0
- package/dist/headless/parts.d.ts +11 -0
- package/dist/headless/parts.js +11 -0
- package/dist/headless/question-back.d.ts +2 -0
- package/dist/headless/question-back.js +16 -0
- package/dist/headless/question-error.d.ts +2 -0
- package/dist/headless/question-error.js +14 -0
- package/dist/headless/question-invalid.d.ts +8 -0
- package/dist/headless/question-invalid.js +18 -0
- package/dist/headless/question-item.d.ts +8 -0
- package/dist/headless/question-item.js +51 -0
- package/dist/headless/question-next.d.ts +3 -0
- package/dist/headless/question-next.js +16 -0
- package/dist/headless/question-other.d.ts +5 -0
- package/dist/headless/question-other.js +22 -0
- package/dist/headless/question-progress.d.ts +8 -0
- package/dist/headless/question-progress.js +14 -0
- package/dist/headless/question-receipt.d.ts +11 -0
- package/dist/headless/question-receipt.js +29 -0
- package/dist/headless/questions.d.ts +3 -0
- package/dist/headless/questions.js +13 -0
- package/dist/headless/queue-editing.d.ts +4 -0
- package/dist/headless/queue-editing.js +11 -0
- package/dist/headless/queue-item.d.ts +2 -0
- package/dist/headless/queue-item.js +10 -0
- package/dist/headless/queue-items.d.ts +2 -0
- package/dist/headless/queue-items.js +11 -0
- package/dist/headless/queue-preview.d.ts +6 -0
- package/dist/headless/queue-preview.js +11 -0
- package/dist/headless/queued-input.d.ts +2 -0
- package/dist/headless/queued-input.js +27 -0
- package/dist/headless/queued.d.ts +17 -0
- package/dist/headless/queued.js +16 -0
- package/dist/headless/read-aloud.d.ts +5 -0
- package/dist/headless/read-aloud.js +19 -0
- package/dist/headless/reasoning-disclosure.d.ts +3 -0
- package/dist/headless/reasoning-disclosure.js +17 -0
- package/dist/headless/reasoning-timer.d.ts +5 -0
- package/dist/headless/reasoning-timer.js +13 -0
- package/dist/headless/references.d.ts +6 -0
- package/dist/headless/references.js +13 -0
- package/dist/headless/regenerate.d.ts +3 -0
- package/dist/headless/regenerate.js +22 -0
- package/dist/headless/remembered-approval.d.ts +2 -0
- package/dist/headless/remembered-approval.js +14 -0
- package/dist/headless/root.d.ts +30 -0
- package/dist/headless/root.js +99 -0
- package/dist/headless/save-edit.d.ts +2 -0
- package/dist/headless/save-edit.js +16 -0
- package/dist/headless/save-queued.d.ts +2 -0
- package/dist/headless/save-queued.js +15 -0
- package/dist/headless/send-now.d.ts +2 -0
- package/dist/headless/send-now.js +17 -0
- package/dist/headless/settings.d.ts +2 -0
- package/dist/headless/settings.js +10 -0
- package/dist/headless/skip-questions.d.ts +6 -0
- package/dist/headless/skip-questions.js +18 -0
- package/dist/headless/source-context.d.ts +5 -0
- package/dist/headless/source-context.js +9 -0
- package/dist/headless/source-link.d.ts +2 -0
- package/dist/headless/source-link.js +13 -0
- package/dist/headless/source-primitives.d.ts +1 -0
- package/dist/headless/source-primitives.js +7 -0
- package/dist/headless/source-root.d.ts +5 -0
- package/dist/headless/source-root.js +9 -0
- package/dist/headless/source-sheet-close.d.ts +2 -0
- package/dist/headless/source-sheet-close.js +12 -0
- package/dist/headless/source-sheet-content.d.ts +3 -0
- package/dist/headless/source-sheet-content.js +16 -0
- package/dist/headless/source-sheet-description.d.ts +2 -0
- package/dist/headless/source-sheet-description.js +12 -0
- package/dist/headless/source-sheet-root.d.ts +2 -0
- package/dist/headless/source-sheet-root.js +13 -0
- package/dist/headless/source-sheet-title.d.ts +2 -0
- package/dist/headless/source-sheet-title.js +12 -0
- package/dist/headless/source-sheet-trigger.d.ts +2 -0
- package/dist/headless/source-sheet-trigger.js +12 -0
- package/dist/headless/source-unavailable.d.ts +2 -0
- package/dist/headless/source-unavailable.js +9 -0
- package/dist/headless/sources.d.ts +14 -0
- package/dist/headless/sources.js +12 -0
- package/dist/headless/speaker.d.ts +13 -0
- package/dist/headless/speaker.js +10 -0
- package/dist/headless/step-boundary.d.ts +4 -0
- package/dist/headless/step-boundary.js +16 -0
- package/dist/headless/stop.d.ts +10 -0
- package/dist/headless/stop.js +18 -0
- package/dist/headless/stopped.d.ts +11 -0
- package/dist/headless/stopped.js +17 -0
- package/dist/headless/submit-answers.d.ts +3 -0
- package/dist/headless/submit-answers.js +12 -0
- package/dist/headless/submit.d.ts +23 -0
- package/dist/headless/submit.js +24 -0
- package/dist/headless/suggestion.d.ts +10 -0
- package/dist/headless/suggestion.js +14 -0
- package/dist/headless/suggestions.d.ts +8 -0
- package/dist/headless/suggestions.js +12 -0
- package/dist/headless/thread.d.ts +3 -0
- package/dist/headless/thread.js +25 -0
- package/dist/headless/timing.d.ts +5 -0
- package/dist/headless/timing.js +13 -0
- package/dist/headless/toggle-tool.d.ts +4 -0
- package/dist/headless/toggle-tool.js +15 -0
- package/dist/headless/tool-error.d.ts +4 -0
- package/dist/headless/tool-error.js +13 -0
- package/dist/headless/tool-input.d.ts +4 -0
- package/dist/headless/tool-input.js +13 -0
- package/dist/headless/tool-output.d.ts +4 -0
- package/dist/headless/tool-output.js +13 -0
- package/dist/headless/tool-renderer.d.ts +13 -0
- package/dist/headless/tool-renderer.js +15 -0
- package/dist/headless/transcript.d.ts +23 -0
- package/dist/headless/transcript.js +24 -0
- package/dist/headless/try-again.d.ts +2 -0
- package/dist/headless/try-again.js +24 -0
- package/dist/headless/unavailable.d.ts +8 -0
- package/dist/headless/unavailable.js +9 -0
- package/dist/headless/usage.d.ts +13 -0
- package/dist/headless/usage.js +15 -0
- package/dist/headless/use-merged-ref.d.ts +2 -0
- package/dist/headless/use-merged-ref.js +17 -0
- package/dist/headless/use-question.d.ts +3 -0
- package/dist/headless/use-question.js +9 -0
- package/dist/headless/waiting.d.ts +13 -0
- package/dist/headless/waiting.js +12 -0
- package/dist/headless/writing.d.ts +13 -0
- package/dist/headless/writing.js +12 -0
- package/dist/hooks/chat-refusal.d.ts +18 -0
- package/dist/hooks/chat-refusal.js +44 -0
- package/dist/hooks/turn-mailbox.d.ts +14 -0
- package/dist/hooks/turn-mailbox.js +114 -0
- package/dist/hooks/use-branches.d.ts +10 -0
- package/dist/hooks/use-branches.js +69 -0
- package/dist/hooks/use-chat-instance.d.ts +28 -0
- package/dist/hooks/use-chat-instance.js +44 -0
- package/dist/hooks/use-chat-settings.d.ts +11 -0
- package/dist/hooks/use-chat-settings.js +33 -0
- package/dist/hooks/use-chat-sink.d.ts +19 -0
- package/dist/hooks/use-chat-sink.js +56 -0
- package/dist/hooks/use-composer.d.ts +33 -0
- package/dist/hooks/use-composer.js +45 -0
- package/dist/hooks/use-context-usage.d.ts +8 -0
- package/dist/hooks/use-context-usage.js +14 -0
- package/dist/hooks/use-copied.d.ts +14 -0
- package/dist/hooks/use-copied.js +22 -0
- package/dist/hooks/use-draft.d.ts +6 -0
- package/dist/hooks/use-draft.js +38 -0
- package/dist/hooks/use-feedback.d.ts +8 -0
- package/dist/hooks/use-feedback.js +28 -0
- package/dist/hooks/use-follow.d.ts +15 -0
- package/dist/hooks/use-follow.js +30 -0
- package/dist/hooks/use-media-load.d.ts +5 -0
- package/dist/hooks/use-media-load.js +12 -0
- package/dist/hooks/use-media.d.ts +3 -0
- package/dist/hooks/use-media.js +9 -0
- package/dist/hooks/use-question-check.d.ts +11 -0
- package/dist/hooks/use-question-check.js +29 -0
- package/dist/hooks/use-question-draft.d.ts +34 -0
- package/dist/hooks/use-question-draft.js +57 -0
- package/dist/hooks/use-question-steps.d.ts +18 -0
- package/dist/hooks/use-question-steps.js +31 -0
- package/dist/hooks/use-question-write.d.ts +8 -0
- package/dist/hooks/use-question-write.js +25 -0
- package/dist/hooks/use-read-aloud.d.ts +8 -0
- package/dist/hooks/use-read-aloud.js +60 -0
- package/dist/hooks/use-reasoning.d.ts +8 -0
- package/dist/hooks/use-reasoning.js +32 -0
- package/dist/hooks/use-remembered-approvals.d.ts +9 -0
- package/dist/hooks/use-remembered-approvals.js +40 -0
- package/dist/hooks/use-reply.d.ts +3 -0
- package/dist/hooks/use-reply.js +9 -0
- package/dist/hooks/use-report-missing-configuration.d.ts +9 -0
- package/dist/hooks/use-report-missing-configuration.js +14 -0
- package/dist/hooks/use-retry-delay.d.ts +5 -0
- package/dist/hooks/use-retry-delay.js +17 -0
- package/dist/hooks/use-sent-draft.d.ts +8 -0
- package/dist/hooks/use-sent-draft.js +25 -0
- package/dist/hooks/use-source-disclosure.d.ts +5 -0
- package/dist/hooks/use-source-disclosure.js +9 -0
- package/dist/hooks/use-sources.d.ts +3 -0
- package/dist/hooks/use-sources.js +9 -0
- package/dist/hooks/use-tool-approval-memory.d.ts +8 -0
- package/dist/hooks/use-tool-approval-memory.js +16 -0
- package/dist/hooks/use-tool-approval.d.ts +10 -0
- package/dist/hooks/use-tool-approval.js +26 -0
- package/dist/hooks/use-turn-queue.d.ts +32 -0
- package/dist/hooks/use-turn-queue.js +33 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/next.d.ts +43 -0
- package/dist/next.js +10 -0
- package/dist/services/approval-memory.d.ts +9 -0
- package/dist/services/approval-memory.js +32 -0
- package/dist/services/approval.d.ts +14 -0
- package/dist/services/approval.js +37 -0
- package/dist/services/branches.d.ts +27 -0
- package/dist/services/branches.js +131 -0
- package/dist/services/chat-session.d.ts +20 -0
- package/dist/services/chat-session.js +61 -0
- package/dist/services/choices.d.ts +31 -0
- package/dist/services/choices.js +39 -0
- package/dist/services/continue.d.ts +8 -0
- package/dist/services/continue.js +24 -0
- package/dist/services/errors.d.ts +52 -0
- package/dist/services/errors.js +54 -0
- package/dist/services/media.d.ts +9 -0
- package/dist/services/media.js +19 -0
- package/dist/services/messages.d.ts +44 -0
- package/dist/services/messages.js +0 -0
- package/dist/services/optional.d.ts +7 -0
- package/dist/services/optional.js +7 -0
- package/dist/services/ports.d.ts +31 -0
- package/dist/services/ports.js +0 -0
- package/dist/services/questions.d.ts +67 -0
- package/dist/services/questions.js +119 -0
- package/dist/services/request.d.ts +26 -0
- package/dist/services/request.js +85 -0
- package/dist/services/selection.d.ts +10 -0
- package/dist/services/selection.js +49 -0
- package/dist/services/sources.d.ts +11 -0
- package/dist/services/sources.js +29 -0
- package/dist/services/tools.d.ts +11 -0
- package/dist/services/tools.js +0 -0
- package/dist/services/turn.d.ts +84 -0
- package/dist/services/turn.js +0 -0
- package/dist/services/types.d.ts +23 -0
- package/dist/services/types.js +15 -0
- package/package.json +822 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useChat } from "@ai-sdk/react";
|
|
3
|
+
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
4
|
+
import {
|
|
5
|
+
conversationBranches,
|
|
6
|
+
branchPage,
|
|
7
|
+
editedParts,
|
|
8
|
+
awaitingApproval,
|
|
9
|
+
awaitingQuestion
|
|
10
|
+
} from "../services/types.js";
|
|
11
|
+
const EMPTY = [];
|
|
12
|
+
const server = () => EMPTY;
|
|
13
|
+
function useBranches(chat, enabled = true) {
|
|
14
|
+
const { messages, status, setMessages } = useChat({ chat });
|
|
15
|
+
const memory = useMemo(() => conversationBranches(chat), [chat]);
|
|
16
|
+
const locations = useSyncExternalStore(memory.watch, memory.snapshot, server);
|
|
17
|
+
const blocked = !enabled || status === "submitted" || status === "streaming" || awaitingApproval(messages) || awaitingQuestion(messages);
|
|
18
|
+
const available = useCallback(
|
|
19
|
+
() => enabled && chat.status !== "submitted" && chat.status !== "streaming" && !awaitingApproval(chat.messages) && !awaitingQuestion(chat.messages),
|
|
20
|
+
[chat, enabled]
|
|
21
|
+
);
|
|
22
|
+
const regenerate = useCallback(
|
|
23
|
+
(id) => {
|
|
24
|
+
const at = chat.messages.findIndex((message) => message.id === id);
|
|
25
|
+
if (!available() || chat.messages[at]?.role !== "assistant" || !chat.messages.slice(0, at).some((message) => message.role === "user"))
|
|
26
|
+
return;
|
|
27
|
+
if (memory.fork(chat.messages, id)) void chat.regenerate({ messageId: id });
|
|
28
|
+
},
|
|
29
|
+
[chat, memory, available]
|
|
30
|
+
);
|
|
31
|
+
const edit = useCallback(
|
|
32
|
+
(id, text) => {
|
|
33
|
+
if (!available()) return false;
|
|
34
|
+
const message = chat.messages.find((message2) => message2.id === id);
|
|
35
|
+
if (message?.role !== "user") return false;
|
|
36
|
+
const value = text.trim();
|
|
37
|
+
if (!value && !message.parts.some((part) => part.type === "file")) return false;
|
|
38
|
+
const previous = message.parts.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
|
|
39
|
+
if (value === previous.trim()) return true;
|
|
40
|
+
if (!memory.fork(chat.messages, id)) return false;
|
|
41
|
+
void chat.sendMessage({
|
|
42
|
+
messageId: id,
|
|
43
|
+
role: "user",
|
|
44
|
+
parts: editedParts(message, value),
|
|
45
|
+
metadata: message.metadata
|
|
46
|
+
});
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
[chat, memory, available]
|
|
50
|
+
);
|
|
51
|
+
const move = useCallback(
|
|
52
|
+
(id, delta) => {
|
|
53
|
+
if (!available()) return;
|
|
54
|
+
const restored = memory.move(chat.messages, id, delta);
|
|
55
|
+
if (restored) {
|
|
56
|
+
chat.clearError();
|
|
57
|
+
setMessages(restored);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[chat, memory, available, setMessages]
|
|
61
|
+
);
|
|
62
|
+
return useMemo(
|
|
63
|
+
() => ({ blocked, page: (id) => branchPage(messages, id, locations), regenerate, edit, move }),
|
|
64
|
+
[blocked, messages, regenerate, edit, move, locations]
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
useBranches
|
|
69
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Chat } from "@ai-sdk/react";
|
|
2
|
+
import { type ChatSettings } from "../services/types";
|
|
3
|
+
import { type Enabled } from "@slices.sh/core";
|
|
4
|
+
import type { AiChatMessage } from "../services/types";
|
|
5
|
+
/** The AI SDK's own chat, typed with the metadata a route writes. */
|
|
6
|
+
export type AiChat = Chat<AiChatMessage>;
|
|
7
|
+
/**
|
|
8
|
+
* What answers a chat: one the app handed the screen, the installed backend's route, or
|
|
9
|
+
* nothing at all — which only an app with no backend installed ever sees.
|
|
10
|
+
*/
|
|
11
|
+
export type ChatSource = "given" | "installed" | "missing";
|
|
12
|
+
export type ChatInstance = {
|
|
13
|
+
chat: AiChat;
|
|
14
|
+
source: ChatSource;
|
|
15
|
+
settings: ChatSettings | null;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The chat a screen runs: the one it was handed, or one over the installed backend.
|
|
19
|
+
*
|
|
20
|
+
* Handing one over is the AI SDK's own way of owning a conversation — its id, the messages
|
|
21
|
+
* it opens on, a transport with your headers — and a screen that is handed nothing still
|
|
22
|
+
* works, over the route `slices add` installed. Fixed for the mount: another chat is a
|
|
23
|
+
* remount with a `key`, never a new instance mid-turn.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useChatInstance(name: string, given: AiChat | undefined, { config, enabled }?: {
|
|
26
|
+
config?: string;
|
|
27
|
+
enabled?: Enabled;
|
|
28
|
+
}): ChatInstance;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Chat } from "@ai-sdk/react";
|
|
3
|
+
import { chatSession, shouldContinueChat } from "../services/types.js";
|
|
4
|
+
import { useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { enabledOf } from "@slices.sh/core";
|
|
6
|
+
import { getAiChat } from "../services/optional.js";
|
|
7
|
+
import { useReportMissingConfiguration } from "./use-report-missing-configuration.js";
|
|
8
|
+
function useChatInstance(name, given, { config, enabled } = {}) {
|
|
9
|
+
const [own] = useState(() => installed(name, given !== void 0, config, enabled));
|
|
10
|
+
useEffect(() => () => void own.chat.stop(), [own]);
|
|
11
|
+
const instance = useMemo(
|
|
12
|
+
() => given && enabled !== false ? { chat: given, source: "given", settings: null } : own,
|
|
13
|
+
[given, own, enabled]
|
|
14
|
+
);
|
|
15
|
+
useReportMissingConfiguration(instance.chat);
|
|
16
|
+
return instance;
|
|
17
|
+
}
|
|
18
|
+
const MISSING = () => ({ chat: new Chat({}), source: "missing", settings: null });
|
|
19
|
+
function installed(name, handed, config, enabled) {
|
|
20
|
+
if (enabled === false) return MISSING();
|
|
21
|
+
const client = getAiChat(config);
|
|
22
|
+
if (!client) {
|
|
23
|
+
if (!handed) enabledOf({ service: "ai-chat", config, enabled, where: "<Chat.Root>" });
|
|
24
|
+
return MISSING();
|
|
25
|
+
}
|
|
26
|
+
if (typeof enabled === "string" && enabled !== "optional") {
|
|
27
|
+
enabledOf({
|
|
28
|
+
service: "ai-chat",
|
|
29
|
+
config,
|
|
30
|
+
enabled,
|
|
31
|
+
where: "<Chat.Root>",
|
|
32
|
+
granted: (capabilities) => Object.keys(capabilities).filter((key) => capabilities[key])
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const { transport, settings } = chatSession(client, name);
|
|
36
|
+
return {
|
|
37
|
+
chat: new Chat({ transport, sendAutomaticallyWhen: shouldContinueChat }),
|
|
38
|
+
source: "installed",
|
|
39
|
+
settings
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
useChatInstance
|
|
44
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ChatSettings } from "../services/types";
|
|
2
|
+
import type { AiChat } from "./use-chat-instance";
|
|
3
|
+
export declare function useChatSettings(chat: AiChat, settings: ChatSettings | null, waiting: number): {
|
|
4
|
+
blocked: boolean;
|
|
5
|
+
model: (id: string) => void;
|
|
6
|
+
toggle: (id: string) => void;
|
|
7
|
+
removeLast: () => boolean;
|
|
8
|
+
choices: import("../services/choices").ChatChoices | null;
|
|
9
|
+
selection: import("../services/choices").ChatSelection;
|
|
10
|
+
};
|
|
11
|
+
export type ChatSettingsView = ReturnType<typeof useChatSettings>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useChat } from "@ai-sdk/react";
|
|
3
|
+
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
|
4
|
+
import { awaitingApproval, awaitingQuestion } from "../services/types.js";
|
|
5
|
+
const EMPTY = { choices: null, selection: {} };
|
|
6
|
+
const empty = () => EMPTY;
|
|
7
|
+
const watchNothing = () => () => {
|
|
8
|
+
};
|
|
9
|
+
function useChatSettings(chat, settings, waiting) {
|
|
10
|
+
const { status, messages } = useChat({ chat });
|
|
11
|
+
const state = useSyncExternalStore(settings?.watch ?? watchNothing, settings?.snapshot ?? empty, empty);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
void settings?.load();
|
|
14
|
+
}, [settings]);
|
|
15
|
+
const blocked = status === "submitted" || status === "streaming" || waiting > 0 || awaitingApproval(messages) || awaitingQuestion(messages);
|
|
16
|
+
const available = useCallback(() => !waiting && chat.status !== "submitted" && chat.status !== "streaming" && !awaitingApproval(chat.messages) && !awaitingQuestion(chat.messages), [chat, waiting]);
|
|
17
|
+
const model = useCallback((id) => {
|
|
18
|
+
if (available()) settings?.model(id);
|
|
19
|
+
}, [available, settings]);
|
|
20
|
+
const toggle = useCallback((id) => {
|
|
21
|
+
if (available()) settings?.toggle(id);
|
|
22
|
+
}, [available, settings]);
|
|
23
|
+
const removeLast = useCallback(() => {
|
|
24
|
+
const last = settings?.snapshot().selection.tools?.at(-1);
|
|
25
|
+
if (!last || !available()) return false;
|
|
26
|
+
settings.toggle(last);
|
|
27
|
+
return true;
|
|
28
|
+
}, [available, settings]);
|
|
29
|
+
return { ...state, blocked, model, toggle, removeLast };
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
useChatSettings
|
|
33
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FileUIPart } from "ai";
|
|
2
|
+
import type { Sink } from "@slices.sh/composer";
|
|
3
|
+
import type { AiChat, ChatSource } from "./use-chat-instance";
|
|
4
|
+
/**
|
|
5
|
+
* The chat as a destination for Composer: one concern, how a message gets into the
|
|
6
|
+
* conversation. Sending is the queue's `enqueue` while the chat queues, and a direct send
|
|
7
|
+
* (skipping an open question first) when it does not.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useChatSink({ chat, source, config, queue, enqueue, stop, removeLast, }: {
|
|
10
|
+
chat: AiChat;
|
|
11
|
+
source: ChatSource;
|
|
12
|
+
config?: string;
|
|
13
|
+
queue: boolean;
|
|
14
|
+
enqueue(text: string, files?: readonly FileUIPart[]): void;
|
|
15
|
+
/** Stops the streaming reply; false when none was streaming. */
|
|
16
|
+
stop(): boolean;
|
|
17
|
+
/** Takes the last chosen optional tool off the next turn; false when there is none or the chat is busy. */
|
|
18
|
+
removeLast(): boolean;
|
|
19
|
+
}): Sink | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useChat } from "@ai-sdk/react";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { currentRegistry } from "@slices.sh/core";
|
|
5
|
+
import { readFileDataUrl } from "@slices.sh/core/platform/read-file";
|
|
6
|
+
import { skipQuestions } from "../services/types.js";
|
|
7
|
+
function attachmentsOf(config) {
|
|
8
|
+
const files = currentRegistry()?.get("ai-chat", config)?.provider.capabilities?.files;
|
|
9
|
+
if (files === false) return false;
|
|
10
|
+
if (files && typeof files === "object") return files;
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
async function partOf(file) {
|
|
14
|
+
const mediaType = file.type || "application/octet-stream";
|
|
15
|
+
const url = file.source ? await readFileDataUrl(file.source) : file.url;
|
|
16
|
+
if (!url) throw new Error(`${file.name} has no bytes to send`);
|
|
17
|
+
return { type: "file", filename: file.name, mediaType, url };
|
|
18
|
+
}
|
|
19
|
+
function useChatSink({
|
|
20
|
+
chat,
|
|
21
|
+
source,
|
|
22
|
+
config,
|
|
23
|
+
queue,
|
|
24
|
+
enqueue,
|
|
25
|
+
stop,
|
|
26
|
+
removeLast
|
|
27
|
+
}) {
|
|
28
|
+
const { status } = useChat({ chat });
|
|
29
|
+
const running = status === "submitted" || status === "streaming";
|
|
30
|
+
const attachments = attachmentsOf(config);
|
|
31
|
+
const accept = useMemo(() => {
|
|
32
|
+
const open = {};
|
|
33
|
+
if (attachments !== void 0) open.attachments = attachments;
|
|
34
|
+
return open;
|
|
35
|
+
}, [attachments]);
|
|
36
|
+
return useMemo(() => {
|
|
37
|
+
if (source === "missing") return null;
|
|
38
|
+
return {
|
|
39
|
+
async submit(message) {
|
|
40
|
+
const text = (message.text ?? "").trim();
|
|
41
|
+
const files = await Promise.all((message.attachments ?? []).map(partOf));
|
|
42
|
+
if (queue) return enqueue(text, files);
|
|
43
|
+
await skipQuestions(chat);
|
|
44
|
+
await chat.sendMessage({ text, ...files.length ? { files } : {} });
|
|
45
|
+
},
|
|
46
|
+
busy: running,
|
|
47
|
+
whenBusy: queue ? "queue" : "hold",
|
|
48
|
+
accept,
|
|
49
|
+
stop,
|
|
50
|
+
removeLast
|
|
51
|
+
};
|
|
52
|
+
}, [source, queue, running, chat, enqueue, accept, stop, removeLast]);
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
useChatSink
|
|
56
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type KeyboardEvent } from "react";
|
|
2
|
+
export type ComposerAttachments = {
|
|
3
|
+
count: number;
|
|
4
|
+
blocked: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type ComposerPlacement = "standalone" | "embedded";
|
|
7
|
+
export type Composer = {
|
|
8
|
+
draft: string;
|
|
9
|
+
set(text: string): void;
|
|
10
|
+
/** Sends the draft, or the text given instead of it. */
|
|
11
|
+
submit(text?: string): void;
|
|
12
|
+
keyDown(event: KeyboardEvent): void;
|
|
13
|
+
/** Whether the composer refuses everything, whatever is in it. */
|
|
14
|
+
disabled: boolean;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The draft, and the two ways a person sends it.
|
|
18
|
+
*
|
|
19
|
+
* Enter sends and Shift+Enter breaks the line, which is what every chat does and what makes
|
|
20
|
+
* a textarea worth having over an input. `isComposing` is why this lives in a hook rather
|
|
21
|
+
* than in the part: an IME's Enter commits a candidate, and a chat that sent on it would
|
|
22
|
+
* send half a Japanese word.
|
|
23
|
+
*
|
|
24
|
+
* `submit` takes text because a suggestion button has none in the draft yet, and setting the
|
|
25
|
+
* draft then submitting in one handler would read the draft from before the set.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useComposer({ onSend, scope, onStop, onRemoveLast, disabled, }: {
|
|
28
|
+
scope?: object;
|
|
29
|
+
onSend(text: string): void;
|
|
30
|
+
onStop?(): boolean;
|
|
31
|
+
onRemoveLast?(): boolean;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
}): Composer;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useMemo } from "react";
|
|
3
|
+
import { useDraft } from "./use-draft.js";
|
|
4
|
+
function useComposer({
|
|
5
|
+
onSend,
|
|
6
|
+
scope,
|
|
7
|
+
onStop,
|
|
8
|
+
onRemoveLast,
|
|
9
|
+
disabled = false
|
|
10
|
+
}) {
|
|
11
|
+
const { draft, set: setDraft } = useDraft(scope);
|
|
12
|
+
const submit = useCallback(
|
|
13
|
+
(text) => {
|
|
14
|
+
const sending = text ?? draft;
|
|
15
|
+
if (disabled || sending.trim().length === 0) return;
|
|
16
|
+
onSend(sending);
|
|
17
|
+
setDraft("");
|
|
18
|
+
},
|
|
19
|
+
[disabled, draft, onSend, setDraft]
|
|
20
|
+
);
|
|
21
|
+
const keyDown = useCallback(
|
|
22
|
+
(event) => {
|
|
23
|
+
if (event.key === "Backspace" && !event.defaultPrevented && !event.repeat && !event.nativeEvent.isComposing && !event.altKey && !event.metaKey && !event.ctrlKey && !event.shiftKey && !disabled && draft.length === 0 && onRemoveLast?.()) {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (event.key === "Escape" && !event.nativeEvent.isComposing && !draft.trim() && onStop?.()) {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (event.key !== "Enter" || event.shiftKey && !event.metaKey && !event.ctrlKey || event.nativeEvent.isComposing)
|
|
32
|
+
return;
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
submit();
|
|
35
|
+
},
|
|
36
|
+
[submit, draft, onStop, disabled, onRemoveLast]
|
|
37
|
+
);
|
|
38
|
+
return useMemo(
|
|
39
|
+
() => ({ draft, set: setDraft, submit, keyDown, disabled }),
|
|
40
|
+
[draft, submit, keyDown, disabled, setDraft]
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
useComposer
|
|
45
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AiChat } from "./use-chat-instance";
|
|
2
|
+
export type ContextUsage = {
|
|
3
|
+
tokens: number;
|
|
4
|
+
capacity: number;
|
|
5
|
+
percent: number;
|
|
6
|
+
};
|
|
7
|
+
/** A completed request measurement, not a prediction for the draft or the next turn. */
|
|
8
|
+
export declare function useContextUsage(chat: AiChat): ContextUsage | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useChat } from "@ai-sdk/react";
|
|
3
|
+
function useContextUsage(chat) {
|
|
4
|
+
const { messages, status } = useChat({ chat });
|
|
5
|
+
if (status === "submitted" || status === "streaming") return null;
|
|
6
|
+
const latest = messages.findLast((message) => message.role === "assistant");
|
|
7
|
+
const tokens = latest?.metadata?.contextTokens;
|
|
8
|
+
const capacity = latest?.metadata?.contextWindow;
|
|
9
|
+
if (typeof tokens !== "number" || !Number.isSafeInteger(tokens) || tokens < 0 || typeof capacity !== "number" || !Number.isSafeInteger(capacity) || capacity <= 0) return null;
|
|
10
|
+
return { tokens, capacity, percent: Math.min(100, Math.round(tokens / capacity * 100)) };
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
useContextUsage
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type Copied = {
|
|
2
|
+
/** True for a moment after a successful copy, and never after a refused one. */
|
|
3
|
+
copied: boolean;
|
|
4
|
+
copy(text: string): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Put a message on the clipboard, and say so for two seconds.
|
|
8
|
+
*
|
|
9
|
+
* The flag is the whole reason this is a hook: a copy button with no acknowledgement is a
|
|
10
|
+
* button people press twice. It is set only once the write resolves, because a browser that
|
|
11
|
+
* refuses the clipboard — an insecure origin, a denied permission — would otherwise say
|
|
12
|
+
* "Copied" about nothing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useCopied(): Copied;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
const SHOWN = 2e3;
|
|
4
|
+
function useCopied() {
|
|
5
|
+
const [copied, setCopied] = useState(false);
|
|
6
|
+
const timer = useRef(null);
|
|
7
|
+
useEffect(() => () => void (timer.current && clearTimeout(timer.current)), []);
|
|
8
|
+
const copy = useCallback((text) => {
|
|
9
|
+
void navigator.clipboard?.writeText(text).then(
|
|
10
|
+
() => {
|
|
11
|
+
setCopied(true);
|
|
12
|
+
if (timer.current) clearTimeout(timer.current);
|
|
13
|
+
timer.current = setTimeout(() => setCopied(false), SHOWN);
|
|
14
|
+
},
|
|
15
|
+
() => setCopied(false)
|
|
16
|
+
);
|
|
17
|
+
}, []);
|
|
18
|
+
return { copied, copy };
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
useCopied
|
|
22
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useSyncExternalStore } from "react";
|
|
3
|
+
const drafts = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
function createDraft() {
|
|
5
|
+
let text = "";
|
|
6
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
return {
|
|
8
|
+
read: () => text,
|
|
9
|
+
subscribe: (listener) => {
|
|
10
|
+
listeners.add(listener);
|
|
11
|
+
return () => {
|
|
12
|
+
listeners.delete(listener);
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
set: (next) => {
|
|
16
|
+
if (next === text) return;
|
|
17
|
+
text = next;
|
|
18
|
+
listeners.forEach((listener) => listener());
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function draftFor(scope) {
|
|
23
|
+
let store = drafts.get(scope);
|
|
24
|
+
if (!store) {
|
|
25
|
+
store = createDraft();
|
|
26
|
+
drafts.set(scope, store);
|
|
27
|
+
}
|
|
28
|
+
return store;
|
|
29
|
+
}
|
|
30
|
+
function useDraft(scope) {
|
|
31
|
+
const [local] = useState(createDraft);
|
|
32
|
+
const store = scope ? draftFor(scope) : local;
|
|
33
|
+
const draft = useSyncExternalStore(store.subscribe, store.read, store.read);
|
|
34
|
+
return { draft, set: store.set };
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
useDraft
|
|
38
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChatFeedback, ChatFeedbackOptions } from "../services/types";
|
|
2
|
+
export declare function useFeedback(target: Pick<ChatFeedback, "chatId" | "messageId">, onFeedback: ChatFeedbackOptions["onFeedback"]): {
|
|
3
|
+
rating: "up" | "down" | null;
|
|
4
|
+
pending: boolean;
|
|
5
|
+
failed: boolean;
|
|
6
|
+
submit: (choice: "up" | "down") => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export type Feedback = ReturnType<typeof useFeedback>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
function useFeedback(target, onFeedback) {
|
|
4
|
+
const [rating, setRating] = useState(null);
|
|
5
|
+
const [pending, setPending] = useState(false);
|
|
6
|
+
const [failed, setFailed] = useState(false);
|
|
7
|
+
const busy = useRef(false);
|
|
8
|
+
const submit = async (choice) => {
|
|
9
|
+
if (!onFeedback || busy.current) return;
|
|
10
|
+
busy.current = true;
|
|
11
|
+
setPending(true);
|
|
12
|
+
setFailed(false);
|
|
13
|
+
const next = rating === choice ? null : choice;
|
|
14
|
+
try {
|
|
15
|
+
await onFeedback({ ...target, rating: next });
|
|
16
|
+
setRating(next);
|
|
17
|
+
} catch {
|
|
18
|
+
setFailed(true);
|
|
19
|
+
} finally {
|
|
20
|
+
busy.current = false;
|
|
21
|
+
setPending(false);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return { rating, pending, failed, submit };
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
useFeedback
|
|
28
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Follow = {
|
|
2
|
+
following: boolean;
|
|
3
|
+
jump(): void;
|
|
4
|
+
/** The reader moved the transcript themselves: a wheel, a touch, a key, its scrollbar. */
|
|
5
|
+
release(): void;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The primitive owns anchoring and resize handling; this hook only exposes its controls.
|
|
9
|
+
*
|
|
10
|
+
* It opens a transcript, and places each new question, with the question at the top and its
|
|
11
|
+
* answer running on below: short of the end, yet nowhere the reader chose to go, so a way back
|
|
12
|
+
* to the end would point at a place nobody left. The transcript counts as followed there until
|
|
13
|
+
* the reader moves it, and again once they jump back or `turn`, the newest question, changes.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useFollow(turn?: string): Follow;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useMemo, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { useMessageScroller, useMessageScrollerScrollable } from "@shadcn/react/message-scroller";
|
|
4
|
+
const REDUCED = "(prefers-reduced-motion: reduce)";
|
|
5
|
+
function useFollow(turn = "") {
|
|
6
|
+
const reduced = useSyncExternalStore(watchReducedMotion, prefersReducedMotion, () => false);
|
|
7
|
+
const { scrollToEnd } = useMessageScroller();
|
|
8
|
+
const { end } = useMessageScrollerScrollable();
|
|
9
|
+
const [left, setLeft] = useState(null);
|
|
10
|
+
const held = left !== turn;
|
|
11
|
+
const release = useCallback(() => setLeft(turn), [turn]);
|
|
12
|
+
const jump = useCallback(() => {
|
|
13
|
+
setLeft(null);
|
|
14
|
+
scrollToEnd({ behavior: reduced ? "auto" : "smooth" });
|
|
15
|
+
}, [scrollToEnd, reduced]);
|
|
16
|
+
return useMemo(() => ({ following: !end || held, jump, release }), [end, held, jump, release]);
|
|
17
|
+
}
|
|
18
|
+
function prefersReducedMotion() {
|
|
19
|
+
return typeof window !== "undefined" && window.matchMedia(REDUCED).matches;
|
|
20
|
+
}
|
|
21
|
+
function watchReducedMotion(changed) {
|
|
22
|
+
if (typeof window === "undefined") return () => {
|
|
23
|
+
};
|
|
24
|
+
const query = window.matchMedia(REDUCED);
|
|
25
|
+
query.addEventListener("change", changed);
|
|
26
|
+
return () => query.removeEventListener("change", changed);
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
useFollow
|
|
30
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
function useMediaLoad(url) {
|
|
4
|
+
const [state, setState] = useState({ url, failed: false });
|
|
5
|
+
if (state.url !== url) setState({ url, failed: false });
|
|
6
|
+
const fail = useCallback(() => setState({ url, failed: true }), [url]);
|
|
7
|
+
const failed = state.url === url && state.failed;
|
|
8
|
+
return useMemo(() => ({ failed, fail }), [failed, fail]);
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
useMediaLoad
|
|
12
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type QuestionProblem, type Questionnaire } from "../services/types";
|
|
2
|
+
import type { QuestionDraft } from "./use-question-draft";
|
|
3
|
+
export type QuestionCheck = {
|
|
4
|
+
/** What is wrong with a question's answer right now; null when nothing is. */
|
|
5
|
+
problem(name: string): QuestionProblem | null;
|
|
6
|
+
/** The same, but only once the person tried to go on, so a form never opens covered in errors. */
|
|
7
|
+
shown(name: string): QuestionProblem | null;
|
|
8
|
+
/** Starts showing these questions' problems, and says whether any of them has one. */
|
|
9
|
+
reveal(names: readonly string[]): boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function useQuestionCheck(input: Questionnaire, draft: QuestionDraft): QuestionCheck;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { questionProblem } from "../services/types.js";
|
|
4
|
+
function useQuestionCheck(input, draft) {
|
|
5
|
+
const [revealed, setRevealed] = useState(/* @__PURE__ */ new Set());
|
|
6
|
+
const problem = useCallback(
|
|
7
|
+
(name) => {
|
|
8
|
+
const question = input.questions.find((q) => q.name === name);
|
|
9
|
+
if (!question) return null;
|
|
10
|
+
const other = draft.other(name);
|
|
11
|
+
return questionProblem(question, draft.answers[name], other.on && !other.text.trim());
|
|
12
|
+
},
|
|
13
|
+
[input, draft]
|
|
14
|
+
);
|
|
15
|
+
const reveal = useCallback(
|
|
16
|
+
(names) => {
|
|
17
|
+
setRevealed((current) => /* @__PURE__ */ new Set([...current, ...names]));
|
|
18
|
+
return names.some((name) => problem(name) !== null);
|
|
19
|
+
},
|
|
20
|
+
[problem]
|
|
21
|
+
);
|
|
22
|
+
return useMemo(
|
|
23
|
+
() => ({ problem, shown: (name) => revealed.has(name) ? problem(name) : null, reveal }),
|
|
24
|
+
[problem, revealed, reveal]
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
useQuestionCheck
|
|
29
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { QuestionAnswers, Questionnaire } from "../services/types";
|
|
2
|
+
export { isSkippedQuestion, questionInput } from "../services/types";
|
|
3
|
+
export type DraftChange =
|
|
4
|
+
/** Replaces the choice of a single question and toggles one of a multiple question. */
|
|
5
|
+
{
|
|
6
|
+
type: "choose";
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
/** The Other control: it replaces the choice of a single question and toggles beside a multiple one. */
|
|
10
|
+
| {
|
|
11
|
+
type: "toggle-other";
|
|
12
|
+
}
|
|
13
|
+
/** A click into Other's field chooses it without ever toggling it off. */
|
|
14
|
+
| {
|
|
15
|
+
type: "select-other";
|
|
16
|
+
}
|
|
17
|
+
/** Typing in Other is choosing it. */
|
|
18
|
+
| {
|
|
19
|
+
type: "write-other";
|
|
20
|
+
text: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "clear";
|
|
23
|
+
};
|
|
24
|
+
export type QuestionDraft = {
|
|
25
|
+
/** What would be sent: only the questions that have something in them. */
|
|
26
|
+
answers: QuestionAnswers;
|
|
27
|
+
picked(name: string, value: string): boolean;
|
|
28
|
+
other(name: string): {
|
|
29
|
+
on: boolean;
|
|
30
|
+
text: string;
|
|
31
|
+
};
|
|
32
|
+
change(name: string, change: DraftChange): void;
|
|
33
|
+
};
|
|
34
|
+
export declare function useQuestionDraft(input: Questionnaire): QuestionDraft;
|