@hogoshi/ui 0.1.10 → 0.1.11
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 +1 -1
- package/src/components/chat.tsx +70 -57
- package/src/index.ts +1 -0
package/package.json
CHANGED
package/src/components/chat.tsx
CHANGED
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
Alert01Icon,
|
|
5
5
|
ArrowDown01Icon,
|
|
6
6
|
ArrowUp02Icon,
|
|
7
|
+
Attachment01Icon,
|
|
7
8
|
Cancel01Icon,
|
|
8
9
|
File01Icon,
|
|
9
10
|
FlashIcon,
|
|
10
|
-
Pin02Icon,
|
|
11
11
|
StopCircleIcon,
|
|
12
12
|
Tick02Icon,
|
|
13
|
+
Upload04Icon,
|
|
13
14
|
} from "@hugeicons-pro/core-stroke-rounded";
|
|
14
|
-
import { Pin02Icon as Pin02SolidIcon } from "@hugeicons-pro/core-solid-rounded";
|
|
15
15
|
import { useEffect, useRef, useState, type ComponentProps, type ReactNode } from "react";
|
|
16
16
|
import { cn } from "../lib/cn";
|
|
17
17
|
import type { AppIcon } from "./icon";
|
|
@@ -20,6 +20,7 @@ import { Image, ImageGallery, ImageViewer, type ViewerImage } from "./image-view
|
|
|
20
20
|
import { Button } from "./button";
|
|
21
21
|
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "./collapsible";
|
|
22
22
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./dropdown-menu";
|
|
23
|
+
import { Separator } from "./separator";
|
|
23
24
|
import { Spinner } from "./spinner";
|
|
24
25
|
|
|
25
26
|
const workingText =
|
|
@@ -277,7 +278,6 @@ export interface ChatContextItem {
|
|
|
277
278
|
label: string;
|
|
278
279
|
src?: string;
|
|
279
280
|
icon?: AppIcon;
|
|
280
|
-
pinned?: boolean;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
export interface ChatModelOption {
|
|
@@ -286,15 +286,7 @@ export interface ChatModelOption {
|
|
|
286
286
|
hint?: string;
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
function ContextChip({
|
|
290
|
-
item,
|
|
291
|
-
onTogglePin,
|
|
292
|
-
onRemove,
|
|
293
|
-
}: {
|
|
294
|
-
item: ChatContextItem;
|
|
295
|
-
onTogglePin?: () => void;
|
|
296
|
-
onRemove?: () => void;
|
|
297
|
-
}) {
|
|
289
|
+
function ContextChip({ item, onRemove }: { item: ChatContextItem; onRemove?: () => void }) {
|
|
298
290
|
return (
|
|
299
291
|
<span className="border-border bg-bg-subtle text-fg inline-flex h-7 max-w-[16rem] min-w-0 items-center gap-1.5 rounded-full border py-1 pr-1 pl-2.5">
|
|
300
292
|
{item.src ? (
|
|
@@ -303,20 +295,6 @@ function ContextChip({
|
|
|
303
295
|
<Icon icon={item.icon ?? File01Icon} variant="stroke" size={12} className="text-fg-muted shrink-0" />
|
|
304
296
|
)}
|
|
305
297
|
<span className="min-w-0 truncate text-xs">{item.label}</span>
|
|
306
|
-
{onTogglePin ? (
|
|
307
|
-
<button
|
|
308
|
-
type="button"
|
|
309
|
-
aria-label={item.pinned ? `Unpin ${item.label}` : `Pin ${item.label}`}
|
|
310
|
-
aria-pressed={item.pinned}
|
|
311
|
-
onClick={onTogglePin}
|
|
312
|
-
className={cn(
|
|
313
|
-
"text-fg-muted hover:text-fg focus-visible:ring-ring inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full outline-none focus-visible:ring-2",
|
|
314
|
-
item.pinned && "text-accent",
|
|
315
|
-
)}
|
|
316
|
-
>
|
|
317
|
-
<Icon icon={item.pinned ? Pin02SolidIcon : Pin02Icon} size={12} />
|
|
318
|
-
</button>
|
|
319
|
-
) : null}
|
|
320
298
|
{onRemove ? (
|
|
321
299
|
<button
|
|
322
300
|
type="button"
|
|
@@ -347,8 +325,10 @@ function ModelMenu({
|
|
|
347
325
|
<Button size="sm" variant="ghost" aria-label="Select model" className="text-fg-muted h-7 px-1.5" />
|
|
348
326
|
}
|
|
349
327
|
>
|
|
350
|
-
<
|
|
351
|
-
|
|
328
|
+
<span className="inline-flex items-center gap-1.5">
|
|
329
|
+
<Icon icon={FlashIcon} variant="stroke" size={14} className="text-fg-subtle shrink-0" />
|
|
330
|
+
<span className="max-w-40 truncate text-xs">{model.label}</span>
|
|
331
|
+
</span>
|
|
352
332
|
</DropdownMenuTrigger>
|
|
353
333
|
<DropdownMenuContent>
|
|
354
334
|
{models.map((option) => (
|
|
@@ -367,6 +347,65 @@ function ModelMenu({
|
|
|
367
347
|
);
|
|
368
348
|
}
|
|
369
349
|
|
|
350
|
+
export function ChatAttach({
|
|
351
|
+
items = [],
|
|
352
|
+
onInsert,
|
|
353
|
+
onUpload,
|
|
354
|
+
accept,
|
|
355
|
+
multiple = true,
|
|
356
|
+
className,
|
|
357
|
+
}: {
|
|
358
|
+
items?: ChatSuggestItem[];
|
|
359
|
+
onInsert?: (id: string) => void;
|
|
360
|
+
onUpload?: (files: File[]) => void;
|
|
361
|
+
accept?: string;
|
|
362
|
+
multiple?: boolean;
|
|
363
|
+
className?: string;
|
|
364
|
+
}) {
|
|
365
|
+
const input = useRef<HTMLInputElement>(null);
|
|
366
|
+
return (
|
|
367
|
+
<>
|
|
368
|
+
<input
|
|
369
|
+
ref={input}
|
|
370
|
+
type="file"
|
|
371
|
+
accept={accept}
|
|
372
|
+
multiple={multiple}
|
|
373
|
+
className="hidden"
|
|
374
|
+
onChange={(event) => {
|
|
375
|
+
const files = [...(event.target.files ?? [])];
|
|
376
|
+
event.target.value = "";
|
|
377
|
+
if (files.length) onUpload?.(files);
|
|
378
|
+
}}
|
|
379
|
+
/>
|
|
380
|
+
<DropdownMenu>
|
|
381
|
+
<DropdownMenuTrigger render={<Button size="icon-sm" variant="ghost" aria-label="Attach" className={className} />}>
|
|
382
|
+
<Icon icon={Attachment01Icon} variant="stroke" />
|
|
383
|
+
</DropdownMenuTrigger>
|
|
384
|
+
<DropdownMenuContent>
|
|
385
|
+
{items.length ? (
|
|
386
|
+
<>
|
|
387
|
+
<p className="text-fg-subtle px-2 py-1.5 text-[11px] font-medium tracking-wider uppercase">Project files</p>
|
|
388
|
+
<div className="max-h-60 overflow-y-auto overscroll-contain">
|
|
389
|
+
{items.map((item) => (
|
|
390
|
+
<DropdownMenuItem key={item.id} onClick={() => onInsert?.(item.id)}>
|
|
391
|
+
<Icon icon={item.icon ?? File01Icon} variant="stroke" size={16} className="text-fg-muted shrink-0" />
|
|
392
|
+
<span className="min-w-0 truncate">{item.label}</span>
|
|
393
|
+
</DropdownMenuItem>
|
|
394
|
+
))}
|
|
395
|
+
</div>
|
|
396
|
+
<Separator className="my-1" />
|
|
397
|
+
</>
|
|
398
|
+
) : null}
|
|
399
|
+
<DropdownMenuItem onClick={() => input.current?.click()}>
|
|
400
|
+
<Icon icon={Upload04Icon} variant="stroke" size={16} className="text-fg-muted shrink-0" />
|
|
401
|
+
Upload from computer
|
|
402
|
+
</DropdownMenuItem>
|
|
403
|
+
</DropdownMenuContent>
|
|
404
|
+
</DropdownMenu>
|
|
405
|
+
</>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
370
409
|
export function ChatComposer({
|
|
371
410
|
value,
|
|
372
411
|
onValueChange,
|
|
@@ -377,7 +416,6 @@ export function ChatComposer({
|
|
|
377
416
|
placeholder,
|
|
378
417
|
disabled,
|
|
379
418
|
context,
|
|
380
|
-
onToggleContextPin,
|
|
381
419
|
onRemoveContext,
|
|
382
420
|
model,
|
|
383
421
|
models,
|
|
@@ -394,7 +432,6 @@ export function ChatComposer({
|
|
|
394
432
|
placeholder?: string;
|
|
395
433
|
disabled?: boolean;
|
|
396
434
|
context?: ChatContextItem[];
|
|
397
|
-
onToggleContextPin?: (id: string) => void;
|
|
398
435
|
onRemoveContext?: (id: string) => void;
|
|
399
436
|
model?: ChatModelOption;
|
|
400
437
|
models?: ChatModelOption[];
|
|
@@ -424,7 +461,6 @@ export function ChatComposer({
|
|
|
424
461
|
<ContextChip
|
|
425
462
|
key={item.id}
|
|
426
463
|
item={item}
|
|
427
|
-
onTogglePin={onToggleContextPin ? () => onToggleContextPin(item.id) : undefined}
|
|
428
464
|
onRemove={onRemoveContext ? () => onRemoveContext(item.id) : undefined}
|
|
429
465
|
/>
|
|
430
466
|
))}
|
|
@@ -559,14 +595,10 @@ export function ChatSuggest({
|
|
|
559
595
|
|
|
560
596
|
export function ChatMedia({
|
|
561
597
|
images,
|
|
562
|
-
pinned = false,
|
|
563
|
-
onPin,
|
|
564
598
|
actions,
|
|
565
599
|
className,
|
|
566
600
|
}: {
|
|
567
601
|
images: ViewerImage[];
|
|
568
|
-
pinned?: boolean;
|
|
569
|
-
onPin?: () => void;
|
|
570
602
|
actions?: ReactNode;
|
|
571
603
|
className?: string;
|
|
572
604
|
}) {
|
|
@@ -579,32 +611,13 @@ export function ChatMedia({
|
|
|
579
611
|
src={images[0].src}
|
|
580
612
|
alt={images[0].alt}
|
|
581
613
|
name={images[0].name}
|
|
582
|
-
className="border-border max-h-
|
|
614
|
+
className="border-border max-h-48 w-auto max-w-full rounded-[var(--radius-md)] border object-cover"
|
|
583
615
|
/>
|
|
584
616
|
) : (
|
|
585
617
|
<ImageGallery images={images} className="min-w-0" />
|
|
586
618
|
)}
|
|
587
|
-
{
|
|
588
|
-
<div
|
|
589
|
-
className={cn(
|
|
590
|
-
"absolute top-2 right-2 flex items-center gap-1 transition-opacity duration-[var(--motion-duration)] ease-[var(--motion-ease)] focus-within:opacity-100 group-hover/media:opacity-100",
|
|
591
|
-
pinned ? "opacity-100" : "opacity-0",
|
|
592
|
-
)}
|
|
593
|
-
>
|
|
594
|
-
{onPin ? (
|
|
595
|
-
<button
|
|
596
|
-
type="button"
|
|
597
|
-
aria-label={pinned ? "Unpin from context" : "Pin to context"}
|
|
598
|
-
aria-pressed={pinned}
|
|
599
|
-
onClick={onPin}
|
|
600
|
-
className={cn(
|
|
601
|
-
"border-border bg-bg-elevated/80 text-fg-muted hover:text-fg pointer-events-auto inline-flex size-7 cursor-pointer items-center justify-center rounded-[var(--radius-sm)] border backdrop-blur-md",
|
|
602
|
-
pinned && "text-accent",
|
|
603
|
-
)}
|
|
604
|
-
>
|
|
605
|
-
<Icon icon={pinned ? Pin02SolidIcon : Pin02Icon} size={14} />
|
|
606
|
-
</button>
|
|
607
|
-
) : null}
|
|
619
|
+
{actions ? (
|
|
620
|
+
<div className="absolute top-2 right-2 flex items-center gap-1 opacity-0 transition-opacity duration-[var(--motion-duration)] ease-[var(--motion-ease)] focus-within:opacity-100 group-hover/media:opacity-100">
|
|
608
621
|
{actions}
|
|
609
622
|
</div>
|
|
610
623
|
) : null}
|