ChatMessageAssistant.svelte 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <script lang="ts">
  2. import { ChatMessageThinkingBlock, MarkdownContent } from '$lib/components/app';
  3. import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
  4. import { isLoading } from '$lib/stores/chat.svelte';
  5. import { fade } from 'svelte/transition';
  6. import { Check, Copy, Package, X } from '@lucide/svelte';
  7. import { Button } from '$lib/components/ui/button';
  8. import { Checkbox } from '$lib/components/ui/checkbox';
  9. import { INPUT_CLASSES } from '$lib/constants/input-classes';
  10. import ChatMessageActions from './ChatMessageActions.svelte';
  11. import Label from '$lib/components/ui/label/label.svelte';
  12. import { config } from '$lib/stores/settings.svelte';
  13. import { copyToClipboard } from '$lib/utils/copy';
  14. interface Props {
  15. class?: string;
  16. deletionInfo: {
  17. totalCount: number;
  18. userMessages: number;
  19. assistantMessages: number;
  20. messageTypes: string[];
  21. } | null;
  22. editedContent?: string;
  23. isEditing?: boolean;
  24. message: DatabaseMessage;
  25. messageContent: string | undefined;
  26. onCancelEdit?: () => void;
  27. onCopy: () => void;
  28. onConfirmDelete: () => void;
  29. onDelete: () => void;
  30. onEdit?: () => void;
  31. onEditKeydown?: (event: KeyboardEvent) => void;
  32. onEditedContentChange?: (content: string) => void;
  33. onNavigateToSibling?: (siblingId: string) => void;
  34. onRegenerate: () => void;
  35. onSaveEdit?: () => void;
  36. onShowDeleteDialogChange: (show: boolean) => void;
  37. onShouldBranchAfterEditChange?: (value: boolean) => void;
  38. showDeleteDialog: boolean;
  39. shouldBranchAfterEdit?: boolean;
  40. siblingInfo?: ChatMessageSiblingInfo | null;
  41. textareaElement?: HTMLTextAreaElement;
  42. thinkingContent: string | null;
  43. }
  44. let {
  45. class: className = '',
  46. deletionInfo,
  47. editedContent = '',
  48. isEditing = false,
  49. message,
  50. messageContent,
  51. onCancelEdit,
  52. onConfirmDelete,
  53. onCopy,
  54. onDelete,
  55. onEdit,
  56. onEditKeydown,
  57. onEditedContentChange,
  58. onNavigateToSibling,
  59. onRegenerate,
  60. onSaveEdit,
  61. onShowDeleteDialogChange,
  62. onShouldBranchAfterEditChange,
  63. showDeleteDialog,
  64. shouldBranchAfterEdit = false,
  65. siblingInfo = null,
  66. textareaElement = $bindable(),
  67. thinkingContent
  68. }: Props = $props();
  69. const processingState = useProcessingState();
  70. </script>
  71. <div
  72. class="text-md group w-full leading-7.5 {className}"
  73. role="group"
  74. aria-label="Assistant message with actions"
  75. >
  76. {#if thinkingContent}
  77. <ChatMessageThinkingBlock
  78. reasoningContent={thinkingContent}
  79. isStreaming={!message.timestamp}
  80. hasRegularContent={!!messageContent?.trim()}
  81. />
  82. {/if}
  83. {#if message?.role === 'assistant' && isLoading() && !message?.content?.trim()}
  84. <div class="mt-6 w-full max-w-[48rem]" in:fade>
  85. <div class="processing-container">
  86. <span class="processing-text">
  87. {processingState.getProcessingMessage()}
  88. </span>
  89. </div>
  90. </div>
  91. {/if}
  92. {#if isEditing}
  93. <div class="w-full">
  94. <textarea
  95. bind:this={textareaElement}
  96. bind:value={editedContent}
  97. class="min-h-[50vh] w-full resize-y rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
  98. onkeydown={onEditKeydown}
  99. oninput={(e) => onEditedContentChange?.(e.currentTarget.value)}
  100. placeholder="Edit assistant message..."
  101. ></textarea>
  102. <div class="mt-2 flex items-center justify-between">
  103. <div class="flex items-center space-x-2">
  104. <Checkbox
  105. id="branch-after-edit"
  106. bind:checked={shouldBranchAfterEdit}
  107. onCheckedChange={(checked) => onShouldBranchAfterEditChange?.(checked === true)}
  108. />
  109. <Label for="branch-after-edit" class="cursor-pointer text-sm text-muted-foreground">
  110. Branch conversation after edit
  111. </Label>
  112. </div>
  113. <div class="flex gap-2">
  114. <Button class="h-8 px-3" onclick={onCancelEdit} size="sm" variant="outline">
  115. <X class="mr-1 h-3 w-3" />
  116. Cancel
  117. </Button>
  118. <Button class="h-8 px-3" onclick={onSaveEdit} disabled={!editedContent?.trim()} size="sm">
  119. <Check class="mr-1 h-3 w-3" />
  120. Save
  121. </Button>
  122. </div>
  123. </div>
  124. </div>
  125. {:else if message.role === 'assistant'}
  126. <MarkdownContent content={messageContent || ''} />
  127. {:else}
  128. <div class="text-sm whitespace-pre-wrap">
  129. {messageContent}
  130. </div>
  131. {/if}
  132. {#if config().showModelInfo && message.model}
  133. <span class="mt-6 mb-4 inline-flex items-center gap-1 text-xs text-muted-foreground">
  134. <Package class="h-3.5 w-3.5" />
  135. <span>Model used:</span>
  136. <button
  137. class="inline-flex cursor-pointer items-center gap-1 rounded-sm bg-muted-foreground/15 px-1.5 py-0.75"
  138. onclick={() => copyToClipboard(message.model)}
  139. >
  140. {message.model}
  141. <Copy class="ml-1 h-3 w-3 " />
  142. </button>
  143. </span>
  144. {/if}
  145. {#if message.timestamp && !isEditing}
  146. <ChatMessageActions
  147. role="assistant"
  148. justify="start"
  149. actionsPosition="left"
  150. {siblingInfo}
  151. {showDeleteDialog}
  152. {deletionInfo}
  153. {onCopy}
  154. {onEdit}
  155. {onRegenerate}
  156. {onDelete}
  157. {onConfirmDelete}
  158. {onNavigateToSibling}
  159. {onShowDeleteDialogChange}
  160. />
  161. {/if}
  162. </div>
  163. <style>
  164. .processing-container {
  165. display: flex;
  166. flex-direction: column;
  167. align-items: flex-start;
  168. gap: 0.5rem;
  169. }
  170. .processing-text {
  171. background: linear-gradient(
  172. 90deg,
  173. var(--muted-foreground),
  174. var(--foreground),
  175. var(--muted-foreground)
  176. );
  177. background-size: 200% 100%;
  178. background-clip: text;
  179. -webkit-background-clip: text;
  180. -webkit-text-fill-color: transparent;
  181. animation: shine 1s linear infinite;
  182. font-weight: 500;
  183. font-size: 0.875rem;
  184. }
  185. @keyframes shine {
  186. to {
  187. background-position: -200% 0;
  188. }
  189. }
  190. </style>