268 lines
7.7 KiB
Svelte
268 lines
7.7 KiB
Svelte
<script lang="ts">
|
|
import Code from './Code.svelte'
|
|
import { createEventDispatcher, onMount } from 'svelte'
|
|
import { deleteMessage, chatsStorage, deleteSummaryMessage, truncateFromMessage, submitExitingPromptsNow } from './Storage.svelte'
|
|
import { getPrice } from './Stats.svelte'
|
|
import SvelteMarkdown from 'svelte-markdown'
|
|
import type { Message, Model, Chat } from './Types.svelte'
|
|
import Fa from 'svelte-fa/src/fa.svelte'
|
|
import { faTrash, faDiagramPredecessor, faDiagramNext, faCircleCheck, faPaperPlane } from '@fortawesome/free-solid-svg-icons/index'
|
|
import { scrollIntoViewWithOffset } from './Util.svelte'
|
|
|
|
export let message:Message
|
|
export let chatId:number
|
|
|
|
|
|
$: chat = $chatsStorage.find((chat) => chat.id === chatId) as Chat
|
|
$: chatSettings = chat.settings
|
|
|
|
// Marked options
|
|
const markdownOptions = {
|
|
gfm: true, // Use GitHub Flavored Markdown
|
|
breaks: true, // Enable line breaks in markdown
|
|
mangle: false // Do not mangle email addresses
|
|
}
|
|
|
|
const dispatch = createEventDispatcher()
|
|
let editing = false
|
|
let original:string
|
|
let defaultModel:Model
|
|
let noEdit:boolean
|
|
|
|
onMount(() => {
|
|
original = message.content
|
|
defaultModel = chatSettings.model as any
|
|
noEdit = !!message.summarized
|
|
})
|
|
|
|
const edit = () => {
|
|
if (noEdit) return
|
|
editing = true
|
|
setTimeout(() => {
|
|
const el = document.getElementById('edit-' + message.uuid)
|
|
el && el.focus()
|
|
}, 0)
|
|
}
|
|
|
|
let dbnc
|
|
const update = () => {
|
|
clearTimeout(dbnc)
|
|
dbnc = setTimeout(() => { doChange() }, 250)
|
|
}
|
|
|
|
const doChange = () => {
|
|
if (message.content !== original) {
|
|
dispatch('change', message)
|
|
}
|
|
}
|
|
|
|
const exit = () => {
|
|
doChange()
|
|
editing = false
|
|
}
|
|
|
|
const keydown = (event:KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault()
|
|
message.content = original
|
|
editing = false
|
|
}
|
|
}
|
|
|
|
const scrollToMessage = (uuid:string | string[] | undefined) => {
|
|
if (Array.isArray(uuid)) {
|
|
uuid = uuid[0]
|
|
}
|
|
if (!uuid) {
|
|
console.error('Not a valid uuid', uuid)
|
|
return
|
|
}
|
|
const el = document.getElementById('message-' + uuid)
|
|
if (el) {
|
|
scrollIntoViewWithOffset(el, 80)
|
|
} else {
|
|
console.error("Can't find element with message ID", uuid)
|
|
}
|
|
}
|
|
|
|
// Double click for mobile support
|
|
let lastTap: number = 0
|
|
const editOnDoubleTap = () => {
|
|
const now: number = new Date().getTime()
|
|
const timesince: number = now - lastTap
|
|
if ((timesince < 400) && (timesince > 0)) {
|
|
edit()
|
|
}
|
|
lastTap = new Date().getTime()
|
|
}
|
|
|
|
let waitingForDeleteConfirm:any = 0
|
|
|
|
const checkDelete = () => {
|
|
clearTimeout(waitingForTruncateConfirm); waitingForTruncateConfirm = 0
|
|
if (!waitingForDeleteConfirm) {
|
|
// wait a second for another click to avoid accidental deletes
|
|
waitingForDeleteConfirm = setTimeout(() => { waitingForDeleteConfirm = 0 }, 1000)
|
|
return
|
|
}
|
|
clearTimeout(waitingForDeleteConfirm)
|
|
waitingForDeleteConfirm = 0
|
|
if (message.summarized) {
|
|
// is in a summary, so we're summarized
|
|
window.alert('Sorry, you can\'t delete a summarized message')
|
|
return
|
|
}
|
|
if (message.summary) {
|
|
// We're linked to messages we're a summary of
|
|
if (window.confirm('Are you sure you want to delete this summary?\nYour session may be too long to submit again after you do.')) {
|
|
try {
|
|
deleteSummaryMessage(chatId, message.uuid)
|
|
} catch (e) {
|
|
window.alert('Unable to delete summary:\n' + e.message)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
try {
|
|
deleteMessage(chatId, message.uuid)
|
|
} catch (e) {
|
|
window.alert('Unable to delete:\n' + e.message)
|
|
}
|
|
}
|
|
|
|
|
|
let waitingForTruncateConfirm:any = 0
|
|
|
|
const checkTruncate = () => {
|
|
clearTimeout(waitingForDeleteConfirm); waitingForDeleteConfirm = 0
|
|
if (!waitingForTruncateConfirm) {
|
|
// wait a second for another click to avoid accidental deletes
|
|
waitingForTruncateConfirm = setTimeout(() => { waitingForTruncateConfirm = 0 }, 1000)
|
|
return
|
|
}
|
|
clearTimeout(waitingForTruncateConfirm)
|
|
waitingForTruncateConfirm = 0
|
|
if (message.summarized) {
|
|
// is in a summary, so we're summarized
|
|
window.alert('Sorry, you can\'t truncate a summarized message')
|
|
return
|
|
}
|
|
try {
|
|
truncateFromMessage(chatId, message.uuid)
|
|
$submitExitingPromptsNow = true
|
|
} catch (e) {
|
|
window.alert('Unable to delete:\n' + e.message)
|
|
}
|
|
}
|
|
|
|
</script>
|
|
|
|
{#key message.uuid}
|
|
<article
|
|
id="{'message-' + message.uuid}"
|
|
class="message chat-message"
|
|
class:is-info={message.role === 'user'}
|
|
class:is-success={message.role === 'assistant'}
|
|
class:is-warning={message.role === 'system'}
|
|
class:is-danger={message.role === 'error'}
|
|
class:user-message={message.role === 'user' || message.role === 'system'}
|
|
class:assistant-message={message.role === 'error' || message.role === 'assistant'}
|
|
class:summarized={message.summarized}
|
|
class:editing={editing}
|
|
>
|
|
<div class="message-body content">
|
|
|
|
{#if editing && !noEdit}
|
|
<form class="message-edit" on:submit|preventDefault={update} on:keydown={keydown}>
|
|
<div id={'edit-' + message.uuid} class="message-editor" bind:innerText={message.content} contenteditable
|
|
on:input={update} on:blur={exit} />
|
|
</form>
|
|
{:else}
|
|
<div
|
|
class="message-display"
|
|
|
|
on:touchend={editOnDoubleTap}
|
|
on:dblclick|preventDefault={() => edit()}
|
|
>
|
|
<SvelteMarkdown
|
|
source={message.content}
|
|
options={markdownOptions}
|
|
renderers={{ code: Code, html: Code }}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{#if message.role === 'system'}
|
|
<p class="is-size-7 message-note">System Prompt</p>
|
|
{:else if message.usage}
|
|
<p class="is-size-7 message-note">
|
|
<em>{message.model || defaultModel}</em> using <span class="has-text-weight-bold">{message.usage.total_tokens}</span>
|
|
tokens ~= <span class="has-text-weight-bold">${getPrice(message.usage, message.model || defaultModel).toFixed(6)}</span>
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
<div class="tool-drawer-mask"></div>
|
|
<div class="tool-drawer">
|
|
<div class="button-pack">
|
|
{#if message.summarized}
|
|
<a
|
|
href={'#'}
|
|
title="Jump to summary"
|
|
class="msg-summary-button button is-small is-info"
|
|
on:click|preventDefault={() => {
|
|
scrollToMessage(message.summarized)
|
|
}}
|
|
>
|
|
<span class="icon"><Fa icon={faDiagramNext} /></span>
|
|
</a>
|
|
{/if}
|
|
{#if message.summary}
|
|
<a
|
|
href={'#'}
|
|
title="Jump to summarized"
|
|
class="msg-summarized-button button is-small is-info"
|
|
on:click|preventDefault={() => {
|
|
scrollToMessage(message.summary)
|
|
}}
|
|
>
|
|
<span class="icon"><Fa icon={faDiagramPredecessor} /></span>
|
|
</a>
|
|
{/if}
|
|
{#if !message.summarized}
|
|
<a
|
|
href={'#'}
|
|
title="Delete this message"
|
|
class=" msg-delete-button button is-small is-warning"
|
|
on:click|preventDefault={() => {
|
|
checkDelete()
|
|
}}
|
|
>
|
|
{#if waitingForDeleteConfirm}
|
|
<span class="icon"><Fa icon={faCircleCheck} /></span>
|
|
{:else}
|
|
<span class="icon"><Fa icon={faTrash} /></span>
|
|
{/if}
|
|
</a>
|
|
{/if}
|
|
{#if !message.summarized}
|
|
<a
|
|
href={'#'}
|
|
title="Truncate all and submit"
|
|
class=" msg-delete-button button is-small is-danger"
|
|
on:click|preventDefault={() => {
|
|
checkTruncate()
|
|
}}
|
|
>
|
|
{#if waitingForTruncateConfirm}
|
|
<span class="icon"><Fa icon={faCircleCheck} /></span>
|
|
{:else}
|
|
<span class="icon"><Fa icon={faPaperPlane} /></span>
|
|
{/if}
|
|
</a>
|
|
{/if}
|
|
</div>
|
|
|
|
</div>
|
|
</article>
|
|
{/key}
|
|
|