@djangocfg/ui-tools 2.1.431 → 2.1.433
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/package.json +28 -14
- package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
- package/src/common/CodePanelHeader/index.ts +1 -0
- package/src/common/blocks/BlockError.tsx +46 -0
- package/src/common/blocks/index.ts +34 -0
- package/src/common/blocks/schemas.ts +143 -0
- package/src/common/blocks/types.ts +76 -0
- package/src/common/chips/README.md +67 -0
- package/src/common/chips/UrlChip.tsx +105 -0
- package/src/common/chips/index.ts +20 -0
- package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
- package/src/common/link-preview/index.ts +12 -0
- package/src/common/link-preview/types.ts +44 -0
- package/src/common/tiptap/index.ts +10 -0
- package/src/common/tiptap/useNodeAttrs.ts +128 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
- package/src/tools/chat/README.md +88 -2
- package/src/tools/chat/composer/Composer.tsx +45 -12
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
- package/src/tools/chat/constants.ts +9 -2
- package/src/tools/chat/core/extractFirstUrl.ts +45 -0
- package/src/tools/chat/core/reducer.ts +32 -8
- package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
- package/src/tools/chat/core/transport/mock.ts +4 -1
- package/src/tools/chat/hooks/useChat.ts +20 -20
- package/src/tools/chat/lazy.tsx +10 -0
- package/src/tools/chat/messages/Attachments.tsx +267 -42
- package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
- package/src/tools/chat/messages/MessageBubble.tsx +175 -31
- package/src/tools/chat/messages/MessageList.tsx +107 -21
- package/src/tools/chat/messages/ToolCalls.tsx +28 -13
- package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
- package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
- package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
- package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
- package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
- package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
- package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
- package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
- package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
- package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
- package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
- package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
- package/src/tools/chat/messages/index.ts +13 -0
- package/src/tools/chat/public.ts +5 -1
- package/src/tools/chat/shell/ChatRoot.tsx +29 -0
- package/src/tools/chat/styles/bubbleTokens.ts +18 -0
- package/src/tools/chat/styles/index.ts +1 -0
- package/src/tools/chat/types/attachment.ts +26 -2
- package/src/tools/chat/types/block.ts +56 -9
- package/src/tools/chat/types/blockSchemas.ts +197 -0
- package/src/tools/chat/types/config.ts +33 -0
- package/src/tools/chat/types/events.ts +14 -7
- package/src/tools/chat/types/index.ts +4 -2
- package/src/tools/chat/types/message.ts +7 -0
- package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
- package/src/tools/dev/Map/README.md +30 -345
- package/src/tools/dev/Map/cards/index.ts +6 -0
- package/src/tools/dev/Map/cards/types.ts +67 -0
- package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
- package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
- package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
- package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
- package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
- package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
- package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
- package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
- package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
- package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
- package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
- package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
- package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
- package/src/tools/dev/Map/components/MapControls.tsx +27 -0
- package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
- package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
- package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
- package/src/tools/dev/Map/components/index.ts +35 -1
- package/src/tools/dev/Map/geocode/index.ts +2 -0
- package/src/tools/dev/Map/geocode/photon.ts +265 -0
- package/src/tools/dev/Map/hooks/index.ts +37 -0
- package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
- package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
- package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
- package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
- package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
- package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
- package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
- package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
- package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
- package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
- package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
- package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
- package/src/tools/dev/Map/index.ts +59 -2
- package/src/tools/dev/Map/layers/index.ts +1 -0
- package/src/tools/dev/Map/layers/symbol.ts +93 -0
- package/src/tools/dev/Map/lazy.tsx +120 -0
- package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
- package/src/tools/dev/Map/styles/index.ts +62 -0
- package/src/tools/dev/Map/styles/terrain.ts +29 -0
- package/src/tools/dev/Map/types.ts +31 -3
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
- package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
- package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
- package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
- package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
- package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
- package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
- package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
- package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
- package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
- package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
- package/src/tools/forms/MarkdownEditor/README.md +84 -0
- package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
- package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
- package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
- package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
- package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
- package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
- package/src/tools/forms/MarkdownEditor/index.ts +19 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
- package/src/tools/forms/MarkdownEditor/styles.css +134 -2
- package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
- package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
- package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
- package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
- package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
- package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
- package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
- package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
- package/src/tools/forms/NotionEditor/README.md +109 -188
- package/src/tools/forms/NotionEditor/extensions.ts +15 -0
- package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
- package/src/tools/forms/NotionEditor/styles.css +25 -0
- package/src/tools/forms/NotionEditor/types.ts +9 -0
- package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
- package/src/tools/input/SpeechRecognition/types.ts +10 -0
- package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
- package/src/tools/media/ImageViewer/README.md +9 -0
- package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
- package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
- package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
- package/src/tools/media/ImageViewer/components/index.ts +2 -0
- package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
- package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
- package/src/tools/media/ImageViewer/types.ts +10 -0
- package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
- package/src/tools/media/ImageViewer/utils/index.ts +4 -0
- package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
- package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
- package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
- package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
- package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
- package/src/tools/visual/charts/SmoothLine/README.md +97 -0
- package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
- package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
- package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
- package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
- package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
- package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
- package/src/tools/visual/indicators/Fps/types.ts +1 -1
- package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
- package/src/tools/dev/Map/components/MapContainer.tsx +0 -198
|
@@ -94,21 +94,34 @@ const SIZE_CLASSES: Record<ComposerSize, {
|
|
|
94
94
|
inlineSlot: string;
|
|
95
95
|
}> = {
|
|
96
96
|
sm: {
|
|
97
|
-
|
|
97
|
+
// Symmetric corner inset — px == py so the trailing send button sits
|
|
98
|
+
// equidistant from the surface's right and bottom edges.
|
|
99
|
+
surface: 'rounded-xl px-2 py-2',
|
|
98
100
|
textarea: 'min-h-7 max-h-44 px-1.5 py-1',
|
|
99
101
|
text: 'text-sm',
|
|
100
102
|
containerPadding: 'px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))]',
|
|
101
103
|
inlineSlot: '[&>:not(textarea)]:h-7',
|
|
102
104
|
},
|
|
103
105
|
md: {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
// Symmetric corner inset — px == py so the trailing send button sits
|
|
107
|
+
// equidistant from the surface's right and bottom edges (was py-1.5,
|
|
108
|
+
// which left the button closer to the bottom than the right).
|
|
109
|
+
surface: 'rounded-2xl px-2 py-2',
|
|
110
|
+
// ~one-line tall (min-h-8) with tight, balanced vertical padding.
|
|
111
|
+
// `py-1.5` (6px) stacked under the surface's own `py-1.5` read like a
|
|
112
|
+
// phantom empty line under a single-line input — `py-1` keeps the
|
|
113
|
+
// text vertically centred without the extra bottom gap.
|
|
114
|
+
textarea: 'min-h-8 max-h-60 px-2 py-1',
|
|
115
|
+
// Comfortable 15px chat body. `text-base sm:text-sm` collapsed to
|
|
116
|
+
// 14px and read too small; 15px matches the rich-editor baseline.
|
|
117
|
+
text: 'text-[15px]',
|
|
107
118
|
containerPadding: 'px-2.5 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]',
|
|
108
119
|
inlineSlot: '[&>:not(textarea)]:h-9',
|
|
109
120
|
},
|
|
110
121
|
lg: {
|
|
111
|
-
|
|
122
|
+
// Symmetric corner inset — px == py so the trailing send button sits
|
|
123
|
+
// equidistant from the surface's right and bottom edges.
|
|
124
|
+
surface: 'rounded-2xl px-3 py-3',
|
|
112
125
|
textarea: 'min-h-12 max-h-72 px-2.5 py-2',
|
|
113
126
|
text: 'text-base',
|
|
114
127
|
containerPadding: 'px-3.5 pt-3 pb-[max(0.875rem,env(safe-area-inset-bottom))]',
|
|
@@ -133,7 +146,9 @@ const APPEARANCE_CLASSES: Record<ComposerAppearance, {
|
|
|
133
146
|
}> = {
|
|
134
147
|
compact: { surface: '', textarea: '', text: '', containerPadding: '' },
|
|
135
148
|
full: {
|
|
136
|
-
|
|
149
|
+
// Symmetric corner inset — px == py so the trailing send button sits
|
|
150
|
+
// equidistant from the surface's right and bottom edges.
|
|
151
|
+
surface: 'rounded-3xl px-3 py-3',
|
|
137
152
|
textarea: 'min-h-10 max-h-80 px-2.5 py-2',
|
|
138
153
|
text: 'text-base',
|
|
139
154
|
containerPadding: 'px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]',
|
|
@@ -337,6 +352,8 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
337
352
|
dragDepth.current = 0;
|
|
338
353
|
setIsDragging(false);
|
|
339
354
|
const dropped = Array.from(e.dataTransfer.files ?? []);
|
|
355
|
+
// eslint-disable-next-line no-console
|
|
356
|
+
console.log('[composer] handleDrop files=%d names=%o', dropped.length, dropped.map((f) => f.name));
|
|
340
357
|
if (dropped.length > 0) attachHandle.attachFiles(dropped);
|
|
341
358
|
};
|
|
342
359
|
|
|
@@ -485,7 +502,20 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
485
502
|
// ── Inline (compact single-row) layout ─────────────────────────────────
|
|
486
503
|
// Drag-drop handlers shared by both layouts. No-op when attaching is
|
|
487
504
|
// disabled — the handlers themselves early-return.
|
|
488
|
-
|
|
505
|
+
//
|
|
506
|
+
// `data-wails-dropzone` is load-bearing inside the Wails desktop shell:
|
|
507
|
+
// its runtime gates DOM file drops on a per-element basis. On `dragover`
|
|
508
|
+
// it walks `elementFromPoint(...).closest('[data-wails-dropzone]')` and
|
|
509
|
+
// sets `dataTransfer.dropEffect = 'copy'` ONLY when the cursor is over a
|
|
510
|
+
// marked element — otherwise it forces `dropEffect = 'none'`, and the
|
|
511
|
+
// browser then REJECTS the `drop` on that element (the React `onDrop`
|
|
512
|
+
// never fires). Marking the composer surface makes the runtime allow the
|
|
513
|
+
// drop here, exactly like the debug drop-test harness. Harmless no-op in
|
|
514
|
+
// a plain browser (it's just an unknown data-attribute there).
|
|
515
|
+
const dragProps: React.HTMLAttributes<HTMLDivElement> & {
|
|
516
|
+
'data-wails-dropzone': string;
|
|
517
|
+
} = {
|
|
518
|
+
'data-wails-dropzone': '',
|
|
489
519
|
onDragEnter: handleDragEnter,
|
|
490
520
|
onDragOver: handleDragOver,
|
|
491
521
|
onDragLeave: handleDragLeave,
|
|
@@ -615,12 +645,15 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
615
645
|
ref={surfaceRef}
|
|
616
646
|
onMouseDown={handleSurfaceMouseDown}
|
|
617
647
|
className={cn(
|
|
618
|
-
// Raised input panel —
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
648
|
+
// Raised input panel — a *gentle* elevation over the chat
|
|
649
|
+
// background, not a hard box. `card` is the "slightly elevated
|
|
650
|
+
// surface" token (one step up from `background`); `muted` is the
|
|
651
|
+
// chips/fills token and sat far too light, reading as a
|
|
652
|
+
// high-contrast block on the dark macOS theme. A softened
|
|
653
|
+
// hairline ring (`border/60`) keeps the edge whisper-quiet
|
|
654
|
+
// (Gemini/ChatGPT/Claude style). Focus firms the ring up.
|
|
622
655
|
'flex flex-col rounded-2xl text-foreground',
|
|
623
|
-
'bg-
|
|
656
|
+
'bg-card ring-1 ring-border/60',
|
|
624
657
|
'focus-within:ring-ring/50',
|
|
625
658
|
'transition-colors',
|
|
626
659
|
// The whole surface reads as a text field — the padding around the
|
|
@@ -12,11 +12,23 @@ import {
|
|
|
12
12
|
import { useRegisterComposer } from '../hooks/useAutoFocusOnStreamEnd';
|
|
13
13
|
import type { ComposerSize, ComposerTextareaProps } from './types';
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Single-row min-height FLOOR per composer size (px).
|
|
17
|
+
*
|
|
18
|
+
* This is a floor, NOT the row height — the empty `<p>`'s own line box
|
|
19
|
+
* defines the real single-row height. The floor must stay BELOW one line
|
|
20
|
+
* box (15px text × line-height) so it can never inflate the editor past
|
|
21
|
+
* one line: when a host overrides the editor leading (e.g. cmdop pins
|
|
22
|
+
* `.ProseMirror` to `leading-normal` ≈ 22.5px), a 24/28px min-height used
|
|
23
|
+
* to sit ABOVE the line box and the empty composer read as having a
|
|
24
|
+
* phantom extra line under the caret. Keeping every floor ≤ the tightest
|
|
25
|
+
* expected single line (≈22px) means the line box always wins and the
|
|
26
|
+
* single-row composer is exactly one line tall in every host.
|
|
27
|
+
*/
|
|
16
28
|
const MIN_HEIGHT: Record<ComposerSize, number> = {
|
|
17
|
-
sm:
|
|
18
|
-
md:
|
|
19
|
-
lg:
|
|
29
|
+
sm: 18,
|
|
30
|
+
md: 20,
|
|
31
|
+
lg: 22,
|
|
20
32
|
};
|
|
21
33
|
|
|
22
34
|
export interface ComposerRichTextareaProps extends ComposerTextareaProps {
|
|
@@ -44,8 +44,15 @@ export const LIMITS = {
|
|
|
44
44
|
attachmentsMax: 10,
|
|
45
45
|
/** Composer history slots. */
|
|
46
46
|
composerHistorySize: 50,
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Coalesce stream tokens within this window before dispatching one
|
|
49
|
+
* aggregated chunk. 33ms ≈ 30fps — enough to feel live while halving
|
|
50
|
+
* the re-parse count now that markdown renders during streaming
|
|
51
|
+
* (block-split + per-block memoization keeps each render ~O(n), but
|
|
52
|
+
* fewer dispatches still means fewer tail re-parses). ai-sdk uses ~50ms
|
|
53
|
+
* for the same reason.
|
|
54
|
+
*/
|
|
55
|
+
streamCoalesceMs: 33,
|
|
49
56
|
/** Default history page size. */
|
|
50
57
|
pageSize: 50,
|
|
51
58
|
/** Virtualize list when >= this many messages (host-controlled threshold). */
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the FIRST previewable http(s) URL from a message's plain text.
|
|
3
|
+
*
|
|
4
|
+
* Telegram/WhatsApp rule: at most one preview per message — we surface the
|
|
5
|
+
* first link only. Returns `null` when there's nothing to unfurl.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately conservative: only absolute http/https URLs (no bare
|
|
8
|
+
* `example.com`, no mailto/ftp), trailing punctuation trimmed, and a length
|
|
9
|
+
* cap to avoid pathological inputs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// http(s) URL up to the first whitespace or closing bracket/quote.
|
|
13
|
+
const URL_RE = /\bhttps?:\/\/[^\s<>"'`)\]}]+/i;
|
|
14
|
+
|
|
15
|
+
/** Trailing punctuation that's almost always sentence/markup, not the URL. */
|
|
16
|
+
const TRAILING = /[.,;:!?)\]}'"»]+$/;
|
|
17
|
+
|
|
18
|
+
export function extractFirstUrl(text: string | undefined | null): string | null {
|
|
19
|
+
if (!text) return null;
|
|
20
|
+
// Skip fenced/inline code-heavy content cheaply: still match, but the cap
|
|
21
|
+
// below guards huge blobs.
|
|
22
|
+
if (text.length > 50_000) return null;
|
|
23
|
+
|
|
24
|
+
const match = URL_RE.exec(text);
|
|
25
|
+
if (!match) return null;
|
|
26
|
+
|
|
27
|
+
let url = match[0].replace(TRAILING, '');
|
|
28
|
+
|
|
29
|
+
// Balance a trailing ")" that belongs to a markdown link `](url)` wrapper:
|
|
30
|
+
// if the URL has more ")" than "(", drop the extras.
|
|
31
|
+
const opens = (url.match(/\(/g) ?? []).length;
|
|
32
|
+
const closes = (url.match(/\)/g) ?? []).length;
|
|
33
|
+
if (closes > opens) {
|
|
34
|
+
url = url.replace(/\)+$/, (m) => m.slice(0, Math.max(0, m.length - (closes - opens))));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate — a malformed match resolves to no preview.
|
|
38
|
+
try {
|
|
39
|
+
const parsed = new URL(url);
|
|
40
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
|
|
41
|
+
return parsed.toString();
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -8,13 +8,23 @@
|
|
|
8
8
|
* - `version` bumps on edit so memo keys invalidate.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { ChatAttachment, ChatMessage,
|
|
11
|
+
import type { ChatAttachment, ChatMessage, ChatToolCall } from '../types';
|
|
12
12
|
|
|
13
13
|
export interface ChatState {
|
|
14
14
|
sessionId: string | null;
|
|
15
15
|
messages: ChatMessage[];
|
|
16
16
|
/** Initial history load in flight. */
|
|
17
17
|
isLoading: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* True once the initial history load has settled (done OR errored). False
|
|
20
|
+
* from mount until then. Lets the UI distinguish "history still loading"
|
|
21
|
+
* from "genuinely empty conversation": render a spinner while
|
|
22
|
+
* `!historyLoaded`, the empty-state only once `historyLoaded` is true and
|
|
23
|
+
* `messages` is empty. `isLoading` alone can't carry this — it is `false`
|
|
24
|
+
* on the very first synchronous render (before the bootstrap effect runs),
|
|
25
|
+
* which would flash the empty-state for a session that does have history.
|
|
26
|
+
*/
|
|
27
|
+
historyLoaded: boolean;
|
|
18
28
|
/** Assistant is generating a reply. */
|
|
19
29
|
isStreaming: boolean;
|
|
20
30
|
/** Older history page in flight. */
|
|
@@ -28,6 +38,7 @@ export const initialState: ChatState = {
|
|
|
28
38
|
sessionId: null,
|
|
29
39
|
messages: [],
|
|
30
40
|
isLoading: false,
|
|
41
|
+
historyLoaded: false,
|
|
31
42
|
isStreaming: false,
|
|
32
43
|
isLoadingMore: false,
|
|
33
44
|
hasMore: true,
|
|
@@ -73,9 +84,12 @@ export type ChatAction =
|
|
|
73
84
|
| {
|
|
74
85
|
type: 'STREAM_DONE';
|
|
75
86
|
id: string;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Generic finalize merge (content/blocks/sources/tokens/attachments/
|
|
89
|
+
* metadata). The single slot the terminal event carries — applied as
|
|
90
|
+
* a `Partial<ChatMessage>` merge. No-op when absent.
|
|
91
|
+
*/
|
|
92
|
+
patch?: Partial<ChatMessage>;
|
|
79
93
|
}
|
|
80
94
|
| { type: 'STREAM_CANCELLED'; id: string; partialText: string; label?: string }
|
|
81
95
|
| { type: 'STREAM_ERROR'; id?: string; message: string }
|
|
@@ -134,6 +148,11 @@ export function reducer(state: ChatState, action: ChatAction): ChatState {
|
|
|
134
148
|
messages: action.messages ?? state.messages,
|
|
135
149
|
hasMore: action.hasMore ?? state.hasMore,
|
|
136
150
|
oldestCursor: action.cursor ?? state.oldestCursor,
|
|
151
|
+
// When the session carries its messages (the autoCreateSession /
|
|
152
|
+
// newSession path commits them here), the initial load is settled.
|
|
153
|
+
// A bare `SESSION_SET` (resume path sets the id, then awaits
|
|
154
|
+
// loadHistory → HISTORY_LOAD_DONE) leaves `historyLoaded` untouched.
|
|
155
|
+
historyLoaded: action.messages !== undefined ? true : state.historyLoaded,
|
|
137
156
|
error: null,
|
|
138
157
|
};
|
|
139
158
|
|
|
@@ -144,6 +163,7 @@ export function reducer(state: ChatState, action: ChatAction): ChatState {
|
|
|
144
163
|
return {
|
|
145
164
|
...state,
|
|
146
165
|
isLoading: false,
|
|
166
|
+
historyLoaded: true,
|
|
147
167
|
messages: action.messages,
|
|
148
168
|
hasMore: action.hasMore,
|
|
149
169
|
oldestCursor: action.cursor,
|
|
@@ -258,10 +278,11 @@ export function reducer(state: ChatState, action: ChatAction): ChatState {
|
|
|
258
278
|
case 'STREAM_DONE': {
|
|
259
279
|
const patched = patchMessageById(state.messages, action.id, (m) => ({
|
|
260
280
|
...m,
|
|
281
|
+
// Generic finalize merge (content/blocks/sources/tokens/attachments/
|
|
282
|
+
// metadata). No-op when `patch` is absent — `message_end` is then a
|
|
283
|
+
// pure terminal that only clears `isStreaming`.
|
|
284
|
+
...(action.patch ?? {}),
|
|
261
285
|
isStreaming: false,
|
|
262
|
-
tokensIn: action.tokensIn ?? m.tokensIn,
|
|
263
|
-
tokensOut: action.tokensOut ?? m.tokensOut,
|
|
264
|
-
sources: action.sources ?? m.sources,
|
|
265
286
|
}));
|
|
266
287
|
// Drop an assistant message that ended up completely empty (no text, no
|
|
267
288
|
// tool calls). This happens in the HITL approval flow: the first stream
|
|
@@ -377,7 +398,10 @@ export function reducer(state: ChatState, action: ChatAction): ChatState {
|
|
|
377
398
|
};
|
|
378
399
|
|
|
379
400
|
case 'ERROR_SET':
|
|
380
|
-
|
|
401
|
+
// An error settles the initial load — a failed bootstrap must not leave
|
|
402
|
+
// a stuck spinner; clear `isLoading` and mark the load resolved so the
|
|
403
|
+
// host can fall through to its error/empty surface.
|
|
404
|
+
return { ...state, error: action.error, isLoading: false, historyLoaded: true };
|
|
381
405
|
|
|
382
406
|
case 'ATTACHMENT_PROGRESS': {
|
|
383
407
|
const messages = patchMessageById(state.messages, action.messageId, (m) => {
|
|
@@ -93,7 +93,10 @@ export function createMockTransport(opts: MockTransportOptions = {}): ChatTransp
|
|
|
93
93
|
await sleep(latency);
|
|
94
94
|
yield { type: 'chunk', delta: piece };
|
|
95
95
|
}
|
|
96
|
-
yield {
|
|
96
|
+
yield {
|
|
97
|
+
type: 'message_end',
|
|
98
|
+
patch: { tokensIn: content.length, tokensOut: reply.length },
|
|
99
|
+
};
|
|
97
100
|
} else {
|
|
98
101
|
for (const ev of reply) {
|
|
99
102
|
if (options.signal.aborted) return;
|
|
@@ -143,30 +143,34 @@ export function useChat(config: UseChatConfig): UseChatReturn {
|
|
|
143
143
|
const t0 = performance.now();
|
|
144
144
|
try {
|
|
145
145
|
if (config.initialSessionId) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
}
|
|
146
|
+
dispatch({
|
|
147
|
+
type: 'SESSION_SET',
|
|
148
|
+
sessionId: config.initialSessionId,
|
|
149
|
+
});
|
|
152
150
|
const page = await transport.loadHistory(config.initialSessionId, null, pageSize);
|
|
153
|
-
if
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
151
|
+
// Commit the loaded history even if THIS effect run was cancelled.
|
|
152
|
+
// Under React StrictMode the first mount's effect is always
|
|
153
|
+
// cancelled (mount → unmount → mount), and the second run skips the
|
|
154
|
+
// fetch because `sessionId` is already in the persisted reducer
|
|
155
|
+
// state (the early-return at the top of the effect). If we bailed
|
|
156
|
+
// here on `cancelled`, `HISTORY_LOAD_DONE` would never dispatch on
|
|
157
|
+
// either run and the resumed history would silently never reach the
|
|
158
|
+
// reducer — the read succeeds but the messages are dropped, so the
|
|
159
|
+
// chat shows the empty-state until a remount re-bootstraps it. The
|
|
160
|
+
// reducer state survives the StrictMode remount, so dispatching the
|
|
161
|
+
// load result regardless is correct (mirrors the autoCreateSession
|
|
162
|
+
// branch below, which already commits post-cancel for the same reason).
|
|
160
163
|
dispatch({
|
|
161
164
|
type: 'HISTORY_LOAD_DONE',
|
|
162
165
|
messages: page.messages,
|
|
163
166
|
hasMore: page.hasMore,
|
|
164
167
|
cursor: page.nextCursor,
|
|
165
168
|
});
|
|
166
|
-
log.bootstrap.success('resumed', {
|
|
169
|
+
log.bootstrap.success(cancelled ? 'resumed (post-cancel)' : 'resumed', {
|
|
167
170
|
sessionId: config.initialSessionId,
|
|
168
171
|
messages: page.messages.length,
|
|
169
172
|
hasMore: page.hasMore,
|
|
173
|
+
cancelled,
|
|
170
174
|
elapsedMs: Math.round(performance.now() - t0),
|
|
171
175
|
});
|
|
172
176
|
return config.initialSessionId;
|
|
@@ -397,14 +401,10 @@ export function useChat(config: UseChatConfig): UseChatReturn {
|
|
|
397
401
|
dispatch({
|
|
398
402
|
type: 'STREAM_DONE',
|
|
399
403
|
id: targetId,
|
|
400
|
-
|
|
401
|
-
tokensOut: ev.tokensOut,
|
|
402
|
-
sources: ev.sources,
|
|
404
|
+
patch: ev.patch,
|
|
403
405
|
});
|
|
404
406
|
log.stream.debug('message_end', {
|
|
405
|
-
|
|
406
|
-
tokensOut: ev.tokensOut,
|
|
407
|
-
sources: ev.sources?.length ?? 0,
|
|
407
|
+
patchKeys: ev.patch ? Object.keys(ev.patch) : [],
|
|
408
408
|
});
|
|
409
409
|
return;
|
|
410
410
|
case 'message_metrics':
|
package/src/tools/chat/lazy.tsx
CHANGED
|
@@ -75,6 +75,16 @@ export * from './composer';
|
|
|
75
75
|
export * from './shell';
|
|
76
76
|
export * from './launcher';
|
|
77
77
|
|
|
78
|
+
// Markdown renderer + plain-text helper — also surfaced from index.ts; the
|
|
79
|
+
// `./chat` package entry resolves to THIS file (lazy.tsx), so a host that
|
|
80
|
+
// composes a custom mention-aware bubble needs them re-exported here too
|
|
81
|
+
// (otherwise the symbols typecheck via index.ts but fail to bundle at runtime).
|
|
82
|
+
export {
|
|
83
|
+
MarkdownMessage,
|
|
84
|
+
extractTextFromChildren,
|
|
85
|
+
type MarkdownMessageProps,
|
|
86
|
+
} from '../dev/code/MarkdownMessage';
|
|
87
|
+
|
|
78
88
|
// Bridge — AI-driven `point` directives (lightweight, no heavy deps).
|
|
79
89
|
export {
|
|
80
90
|
HighlightOverlay,
|