Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b9f8d64d7 | |||
| d48d8163a7 | |||
| bb5795e00d | |||
|
3f81f96bda
|
|||
|
4b99921023
|
|||
|
c9e48687ef
|
|||
|
a836082de8
|
|||
|
5aebd30808
|
|||
|
166052012d
|
|||
|
28ef0c8a07
|
|||
|
886f385835
|
|||
|
8ccaa824dd
|
|||
|
47009b1f0a
|
|||
|
9550d44220
|
@@ -11,7 +11,7 @@ app/ # Nuxt 4 application (SSR)
|
||||
├── layouts/ # Auth and default layouts
|
||||
├── middleware/ # Global auth middleware
|
||||
├── pages/ # File-based routing (/, /auth/*, /agent/*)
|
||||
├── plugins/ # Auth plugins (client/server), remark markdown
|
||||
├── plugins/ # Auth plugins (client/server)
|
||||
├── types/ # TypeScript interfaces
|
||||
└── utils/ # Crypto, model-mapping, search utilities
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
- [x] Make dropdowns a singleton component (work in progress)
|
||||
- [ ] Make the sidebar better :kekdoggo:. It's annoying to manage across routes
|
||||
- [x] Make topics manually renameable
|
||||
- [ ] Surface auto-rename failures through the future toast/error UI; add actionable retry and bounded bulk naming for unnamed/provisional topics
|
||||
- [ ] Improve latex rendering, make single line equations work _well_, and likely make it configurable
|
||||
- [x] Implement appearence settings
|
||||
- [x] Accent colors
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import '~/assets/css/reset.css';
|
||||
import '~/assets/css/base.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
const { user } = useAuth();
|
||||
const { accent, neutral, hinting, refresh: refreshSettings } = await useUserSettings();
|
||||
@@ -24,7 +25,7 @@ watchEffect(() => {
|
||||
});
|
||||
|
||||
if (import.meta.client) {
|
||||
// force shiki into browser rendering only
|
||||
// Force Shiki into browser rendering only.
|
||||
window.sessionStorage.setItem('mdc-shiki-highlighter', 'browser');
|
||||
}
|
||||
</script>
|
||||
|
||||
+10
-5
@@ -81,6 +81,7 @@
|
||||
|
||||
--color-border: color-mix(in srgb, rgba(255, 255, 255, 0.1), var(--color-accent) var(--accent-hinting));
|
||||
--color-border-active: color-mix(in srgb, rgba(255, 255, 255, 0.16), var(--color-accent) var(--accent-hinting));
|
||||
|
||||
}
|
||||
|
||||
:root.light {
|
||||
@@ -107,6 +108,7 @@
|
||||
|
||||
--color-border: color-mix(in srgb, rgba(0, 0, 0, 0.08), var(--color-accent) var(--accent-hinting));
|
||||
--color-border-active: color-mix(in srgb, rgba(0, 0, 0, 0.14), var(--color-accent) var(--accent-hinting));
|
||||
|
||||
}
|
||||
|
||||
/* :root.dark {
|
||||
@@ -316,16 +318,19 @@ button.accent:hover {
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='18' height='18'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.reasoning-container.middle {
|
||||
mask-image: linear-gradient(#000, #000, transparent 0, #000 12%, #000 88%, transparent)
|
||||
.reasoning-container.top {
|
||||
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 calc(100% - 24px), transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, #000 0%, #000 calc(100% - 24px), transparent 100%);
|
||||
}
|
||||
|
||||
.reasoning-container.top {
|
||||
mask-image: linear-gradient(#000, transparent, #000 0, #000 12%, #000 88%, transparent)
|
||||
.reasoning-container.middle {
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 24px, #000 calc(100% - 24px), transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, #000 24px, #000 calc(100% - 24px), transparent 100%);
|
||||
}
|
||||
|
||||
.reasoning-container.bottom {
|
||||
mask-image: linear-gradient(transparent, #000, transparent 0, #000 12%, #000 88%, #000)
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 24px, #000 100%);
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, #000 24px, #000 100%);
|
||||
}
|
||||
|
||||
/* Markdown renderer */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import ImageViewer from '~/components/ImageViewer.vue';
|
||||
import { resolveFileUrl } from '~~/utils/url';
|
||||
|
||||
const props = defineProps<{
|
||||
file: {
|
||||
@@ -9,14 +10,21 @@ const props = defineProps<{
|
||||
status?: 'uploading' | 'uploaded' | 'error';
|
||||
url: string;
|
||||
progress?: number;
|
||||
retryCount?: number;
|
||||
};
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
retry: [];
|
||||
}>();
|
||||
|
||||
const isImage = computed(() => props.file.mimeType.startsWith('image/'));
|
||||
const isVideo = computed(() => props.file.mimeType.startsWith('video/'));
|
||||
const isAudio = computed(() => props.file.mimeType.startsWith('audio/'));
|
||||
const isPdf = computed(() => props.file.mimeType === 'application/pdf');
|
||||
|
||||
const resolvedUrl = computed(() => resolveFileUrl(props.file.url));
|
||||
|
||||
const imageViewerOpen = ref(false);
|
||||
const imageRef = ref<HTMLImageElement | null>(null);
|
||||
const imageViewerOriginRect = ref<DOMRect | null>(null);
|
||||
@@ -45,16 +53,16 @@ function openImageViewer() {
|
||||
<template>
|
||||
<div class="relative h-full w-fit flex">
|
||||
<div v-if="isImage">
|
||||
<img ref="imageRef" :src="props.file.url"
|
||||
<img ref="imageRef" :src="resolvedUrl"
|
||||
class="rounded-lg h-full w-full max-h-36 max-w-64 object-cover cursor-zoom-in hover:opacity-90 transition-opacity"
|
||||
@click="openImageViewer" />
|
||||
<ImageViewer v-if="imageViewerOpen" :src="props.file.url" :alt="props.file.name"
|
||||
<ImageViewer v-if="imageViewerOpen" :src="resolvedUrl" :alt="props.file.name"
|
||||
:origin-rect="imageViewerOriginRect" :origin-element="imageRef"
|
||||
@close="imageViewerOpen = false" />
|
||||
</div>
|
||||
<video v-else-if="isVideo" controls :src="props.file.url"
|
||||
<video v-else-if="isVideo" controls :src="resolvedUrl"
|
||||
class="rounded-lg h-full w-full max-h-36 max-w-64 object-cover" />
|
||||
<audio v-else-if="isAudio" controls :src="props.file.url" class="rounded-lg h-full w-full max-h-36 max-w-64" />
|
||||
<audio v-else-if="isAudio" controls :src="resolvedUrl" class="rounded-lg h-full w-full max-h-36 max-w-64" />
|
||||
|
||||
<div v-else class="flex items-center gap-2 p-2 rounded-lg bg-[var(--color-hover)] min-w-40 max-w-48">
|
||||
<span :class="[fileIcon, 'text-6 text-[var(--color-accent)]']"></span>
|
||||
@@ -68,8 +76,20 @@ function openImageViewer() {
|
||||
class="absolute inset-0 bg-black/50 rounded-lg flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<span class="i-svg-spinners-90-ring-with-bg text-6 text-white block mb-1"></span>
|
||||
<span class="text-xs text-white">{{ file.progress || 0 }}%</span>
|
||||
<span class="text-xs text-white">
|
||||
{{ file.retryCount ? `Retry ${file.retryCount}/3` : `${file.progress || 0}%` }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="file.status === 'error'"
|
||||
class="absolute inset-0 bg-black/60 rounded-lg flex flex-col items-center justify-center gap-1">
|
||||
<span class="i-mynaui-danger-triangle text-6 text-red-400"></span>
|
||||
<span class="text-xs text-red-300">Upload failed</span>
|
||||
<button @click.stop="emit('retry')"
|
||||
class="mt-1 flex items-center gap-1 px-2 py-0.5 rounded-md bg-white/10 hover:bg-white/20 text-xs text-white transition-colors">
|
||||
<span class="i-mynaui-refresh text-3"></span>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,11 +8,13 @@ const props = defineProps<{
|
||||
mimeType: string;
|
||||
status: 'uploading' | 'uploaded' | 'error';
|
||||
url: string;
|
||||
retryCount?: number;
|
||||
}
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [];
|
||||
retry: [];
|
||||
}>();
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -21,12 +23,16 @@ const handleDelete = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative h-full w-fit flex">
|
||||
<Display :file="props.file" />
|
||||
<div class="relative h-full w-fit flex flex-shrink-0">
|
||||
<Display :file="props.file" @retry="emit('retry')" />
|
||||
|
||||
<button @click="handleDelete"
|
||||
class="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 flex items-center justify-center w-4 h-4 rounded-full bg-[var(--bg-base)] border border-[var(--color-border)] text-xs text-[#ff3b3b]">
|
||||
<span class="i-mynaui-x text-3"></span>
|
||||
</button>
|
||||
<div v-if="file.status === 'error'"
|
||||
class="absolute bottom-0 right-0 translate-x-1/4 translate-y-1/4 flex items-center justify-center w-4 h-4 rounded-full bg-red-500">
|
||||
<span class="i-mynaui-danger-triangle text-2.5 text-white"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+230
-66
@@ -1,14 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { BaseMessage } from '~/composables/useChat';
|
||||
import { onMounted, ref, watch, onUnmounted, nextTick, type Ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch, onUnmounted, nextTick, type Ref } from 'vue';
|
||||
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
|
||||
import type { Agent } from '~/composables/useAgents';
|
||||
import type FileSelector from './FileSelector.vue';
|
||||
import { getComposerHeight } from '~/utils/composer-size';
|
||||
|
||||
const { allModels } = await useModels();
|
||||
const { updateAgent, patchAgentLocally } = await useAgents();
|
||||
|
||||
const inputHeight: Ref<string> = ref('auto');
|
||||
const isDrawerOpen = useCookie<boolean>("chat:tools-drawer", { default: () => false });
|
||||
watch(isDrawerOpen, () => {
|
||||
emit("resize");
|
||||
});
|
||||
|
||||
const inputHeight: Ref<string> = ref('48px');
|
||||
const fileSelectorRef = ref<InstanceType<typeof FileSelector> | null>(null);
|
||||
const inputRef = ref<HTMLTextAreaElement | null>(null);
|
||||
let tempInput = '';
|
||||
@@ -19,6 +25,7 @@ const files = ref<{
|
||||
status: 'uploading' | 'uploaded' | 'error';
|
||||
url: string;
|
||||
progress: number;
|
||||
retryCount: number;
|
||||
}[]>([]);
|
||||
const inputValue = defineModel<BaseMessage>({ required: false, default: { content: '', fileIds: [] } });
|
||||
const textAreaValue = ref('');
|
||||
@@ -31,45 +38,72 @@ watch(files, (newFiles) => {
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [value: BaseMessage, model: ModelWithProvider | null];
|
||||
addMessage: [value: BaseMessage, role: 'user' | 'assistant', model: ModelWithProvider | null];
|
||||
cancel: [];
|
||||
resize: [];
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
loading?: boolean;
|
||||
allowManualRole?: boolean;
|
||||
agent: Readonly<Agent> | null;
|
||||
providers?: ProviderWithModels[];
|
||||
}>();
|
||||
|
||||
const searchConfig = ref({
|
||||
enabled: props.agent?.config?.search?.enabled ?? false,
|
||||
maxResults: props.agent?.config?.search?.maxResults ?? 10,
|
||||
rerank: props.agent?.config?.search?.rerank ?? false,
|
||||
});
|
||||
|
||||
const toolsConfig = ref({
|
||||
python: props.agent?.config?.tools?.python ?? false,
|
||||
});
|
||||
|
||||
watch(() => props.agent?.config?.search, (val) => {
|
||||
searchConfig.value = {
|
||||
enabled: val?.enabled ?? false,
|
||||
maxResults: val?.maxResults ?? 10,
|
||||
rerank: val?.rerank ?? false,
|
||||
};
|
||||
}, { deep: true });
|
||||
|
||||
const saveSearchConfig = async () => {
|
||||
watch(() => props.agent?.config?.tools, (val) => {
|
||||
toolsConfig.value = {
|
||||
python: val?.python ?? false,
|
||||
};
|
||||
}, { deep: true });
|
||||
|
||||
const saveAgentConfig = async (partial: {
|
||||
search?: typeof searchConfig.value;
|
||||
tools?: typeof toolsConfig.value;
|
||||
}) => {
|
||||
if (!props.agent) return;
|
||||
const currentConfig = props.agent.config ?? {};
|
||||
const newConfig = {
|
||||
...currentConfig,
|
||||
...(partial.search !== undefined ? {
|
||||
search: {
|
||||
enabled: searchConfig.value.enabled,
|
||||
maxResults: searchConfig.value.maxResults,
|
||||
rerank: searchConfig.value.rerank,
|
||||
enabled: partial.search.enabled,
|
||||
},
|
||||
} : {}),
|
||||
...(partial.tools !== undefined ? {
|
||||
tools: {
|
||||
python: partial.tools.python,
|
||||
},
|
||||
} : {}),
|
||||
};
|
||||
patchAgentLocally(props.agent.id, { config: newConfig });
|
||||
updateAgent(props.agent.id, { config: newConfig });
|
||||
};
|
||||
|
||||
const saveSearchConfig = async () => {
|
||||
await saveAgentConfig({ search: searchConfig.value });
|
||||
};
|
||||
|
||||
const saveToolsConfig = async () => {
|
||||
await saveAgentConfig({ tools: toolsConfig.value });
|
||||
};
|
||||
|
||||
const isUploading = computed(() => files.value.some((f) => f.status === 'uploading'));
|
||||
const hasFailedUploads = computed(() => files.value.some((f) => f.status === 'error'));
|
||||
|
||||
const selectedModel = ref<ModelWithProvider | null>(null);
|
||||
|
||||
const handlePaste = async (event: ClipboardEvent) => {
|
||||
@@ -109,11 +143,18 @@ const initializeModel = () => {
|
||||
const updateAgentDefaultModel = async (modelId: string) => {
|
||||
if (!props.agent) return;
|
||||
try {
|
||||
const currentModelId = props.agent.defaultModelId;
|
||||
await $fetch(`/api/agent/${props.agent.id}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
defaultModelId: modelId,
|
||||
},
|
||||
onRequest: () => {
|
||||
patchAgentLocally(props.agent!.id, { defaultModelId: modelId });
|
||||
},
|
||||
onResponseError: () => {
|
||||
patchAgentLocally(props.agent!.id, { defaultModelId: currentModelId });
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update agent default model:', error);
|
||||
@@ -138,13 +179,45 @@ watch(() => props.agent?.defaultModelId, (newModelId) => {
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (isUploading.value || hasFailedUploads.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.loading) {
|
||||
emit('cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputValue.value.content.trim() || inputValue.value.fileIds.length > 0) {
|
||||
emit('submit', structuredClone(toRaw(inputValue.value)), selectedModel.value);
|
||||
const message: BaseMessage = {
|
||||
content: inputValue.value.content,
|
||||
fileIds: inputValue.value.fileIds,
|
||||
files: files.value.map(f => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
mimeType: f.mimeType,
|
||||
url: f.url,
|
||||
})),
|
||||
};
|
||||
emit('submit', message, selectedModel.value);
|
||||
files.value = [];
|
||||
textAreaValue.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddMessage = (role: 'user' | 'assistant') => {
|
||||
if (inputValue.value.content.trim() || inputValue.value.fileIds.length > 0) {
|
||||
const message: BaseMessage = {
|
||||
content: inputValue.value.content,
|
||||
fileIds: inputValue.value.fileIds,
|
||||
files: files.value.map(f => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
mimeType: f.mimeType,
|
||||
url: f.url,
|
||||
})),
|
||||
};
|
||||
emit('addMessage', message, role, selectedModel.value);
|
||||
files.value = [];
|
||||
textAreaValue.value = '';
|
||||
}
|
||||
@@ -236,47 +309,57 @@ const handleWindowKeyDown = async (event: KeyboardEvent) => {
|
||||
};
|
||||
|
||||
const mirrorRef = ref<HTMLDivElement | null>(null);
|
||||
const MIRROR_PROPERTIES = [
|
||||
'box-sizing', 'font-family', 'font-size', 'font-weight', 'font-style',
|
||||
'line-height', 'letter-spacing', 'word-spacing', 'text-align', 'text-indent',
|
||||
'text-transform', 'tab-size', 'padding-top', 'padding-bottom', 'padding-left',
|
||||
'padding-right', 'border-top-width', 'border-bottom-width', 'border-left-width',
|
||||
'border-right-width', 'border-style', 'white-space', 'overflow-wrap', 'word-break',
|
||||
] as const;
|
||||
let sizingRaf: number | undefined;
|
||||
let sizingObserver: ResizeObserver | undefined;
|
||||
let typographyObserver: MutationObserver | undefined;
|
||||
let sizingDirty = true;
|
||||
let mirrorWidth = '';
|
||||
let lineHeight = 24;
|
||||
let padding = 0;
|
||||
let borders = 0;
|
||||
let borderBox = true;
|
||||
|
||||
const resizeTextArea = () => {
|
||||
if (import.meta.server || sizingRaf !== undefined) return;
|
||||
sizingRaf = requestAnimationFrame(() => {
|
||||
sizingRaf = undefined;
|
||||
const textarea = inputRef.value;
|
||||
const mirror = mirrorRef.value;
|
||||
if (!textarea || !mirror) return;
|
||||
|
||||
if (sizingDirty) {
|
||||
// Finish style reads before changing the isolated measurement box.
|
||||
const styles = getComputedStyle(textarea);
|
||||
const values = MIRROR_PROPERTIES.map(property => styles.getPropertyValue(property));
|
||||
mirrorWidth = styles.width;
|
||||
lineHeight = Number.parseFloat(styles.lineHeight) || Number.parseFloat(styles.fontSize) * 1.2;
|
||||
padding = Number.parseFloat(styles.paddingTop) + Number.parseFloat(styles.paddingBottom);
|
||||
borders = Number.parseFloat(styles.borderTopWidth) + Number.parseFloat(styles.borderBottomWidth);
|
||||
borderBox = styles.boxSizing === 'border-box';
|
||||
MIRROR_PROPERTIES.forEach((property, index) => mirror.style.setProperty(property, values[index]!));
|
||||
mirror.style.width = mirrorWidth;
|
||||
sizingDirty = false;
|
||||
}
|
||||
|
||||
// Preserve the final empty line without adding an extra line to ordinary text.
|
||||
mirror.textContent = textarea.value + '\u200b';
|
||||
const height = `${getComposerHeight(mirror.scrollHeight, lineHeight, padding, borders, borderBox)}px`;
|
||||
if (height === inputHeight.value) return;
|
||||
|
||||
// Capture bottom-follow intent before applying the new height.
|
||||
emit('resize');
|
||||
nextTick().then(() => {
|
||||
mirror.textContent = (textarea.value || '\u200b') + '\n';
|
||||
|
||||
const textareaStyles = getComputedStyle(textarea);
|
||||
mirror.style.width = textareaStyles.width;
|
||||
mirror.style.fontFamily = textareaStyles.fontFamily;
|
||||
mirror.style.fontSize = textareaStyles.fontSize;
|
||||
mirror.style.lineHeight = textareaStyles.lineHeight;
|
||||
mirror.style.letterSpacing = textareaStyles.letterSpacing;
|
||||
mirror.style.wordSpacing = textareaStyles.wordSpacing;
|
||||
mirror.style.textAlign = textareaStyles.textAlign;
|
||||
mirror.style.paddingTop = textareaStyles.paddingTop;
|
||||
mirror.style.paddingBottom = textareaStyles.paddingBottom;
|
||||
mirror.style.paddingLeft = textareaStyles.paddingLeft;
|
||||
mirror.style.paddingRight = textareaStyles.paddingRight;
|
||||
mirror.style.borderTopWidth = textareaStyles.borderTopWidth;
|
||||
mirror.style.borderBottomWidth = textareaStyles.borderBottomWidth;
|
||||
mirror.style.borderLeftWidth = textareaStyles.borderLeftWidth;
|
||||
mirror.style.borderRightWidth = textareaStyles.borderRightWidth;
|
||||
mirror.style.whiteSpace = 'pre-wrap';
|
||||
mirror.style.wordWrap = 'break-word';
|
||||
mirror.style.overflow = 'hidden';
|
||||
|
||||
const lineHeight = 24;
|
||||
const minLines = 2;
|
||||
const maxLines = 10;
|
||||
const minHeight = minLines * lineHeight;
|
||||
const maxHeight = maxLines * lineHeight;
|
||||
const height = Math.min(Math.max(mirror.scrollHeight, minHeight), maxHeight);
|
||||
inputHeight.value = `${height}px`;
|
||||
inputHeight.value = height;
|
||||
});
|
||||
};
|
||||
|
||||
watch(textAreaValue, resizeTextArea, { immediate: true });
|
||||
watch(textAreaValue, resizeTextArea, { flush: 'post' });
|
||||
|
||||
let hasCommandKey = false;
|
||||
if (import.meta.server) {
|
||||
@@ -290,42 +373,53 @@ onBeforeMount(() => {
|
||||
tempInput = (document.getElementById('chat') as HTMLInputElement)?.value ?? '';
|
||||
});
|
||||
|
||||
let resizeRafId: number | undefined;
|
||||
const handleWindowResize = () => {
|
||||
if (resizeRafId) return;
|
||||
resizeRafId = requestAnimationFrame(() => {
|
||||
resizeRafId = undefined;
|
||||
sizingDirty = true;
|
||||
resizeTextArea();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
textAreaValue.value = tempInput;
|
||||
document.addEventListener('keydown', handleWindowKeyDown);
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
inputRef.value?.addEventListener('paste', handlePaste);
|
||||
let observedWidth: number | undefined;
|
||||
sizingObserver = new ResizeObserver(([entry]) => {
|
||||
if (!entry || entry.contentRect.width === observedWidth) return;
|
||||
observedWidth = entry.contentRect.width;
|
||||
handleWindowResize();
|
||||
});
|
||||
if (inputRef.value) sizingObserver.observe(inputRef.value);
|
||||
typographyObserver = new MutationObserver(handleWindowResize);
|
||||
typographyObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style'] });
|
||||
document.fonts.addEventListener('loadingdone', handleWindowResize);
|
||||
handleWindowResize();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleWindowKeyDown);
|
||||
window.removeEventListener('resize', handleWindowResize);
|
||||
if (resizeRafId !== undefined) cancelAnimationFrame(resizeRafId);
|
||||
if (sizingRaf !== undefined) cancelAnimationFrame(sizingRaf);
|
||||
sizingObserver?.disconnect();
|
||||
typographyObserver?.disconnect();
|
||||
document.fonts.removeEventListener('loadingdone', handleWindowResize);
|
||||
inputRef.value?.removeEventListener('paste', handlePaste);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['w-full flex max-h-full', $attrs.class]">
|
||||
<div ref="mirrorRef" aria-hidden="true"
|
||||
class="absolute top-0 left-0 pointer-events-none invisible overflow-hidden h-0"></div>
|
||||
<div class="relative w-full flex flex-shrink-1 flex-col gap-3 p-2 rounded-2xl border transition-border ease-in-out duration-300 bg-[var(--bg-container)]
|
||||
<div :class="['w-full flex flex-col max-h-full', $attrs.class]">
|
||||
<div ref="mirrorRef" data-chat-measurement aria-hidden="true"
|
||||
class="fixed top-0 left-0 pointer-events-none invisible overflow-hidden h-0 [contain:layout_style_paint]"></div>
|
||||
<div class="relative z-10 w-full flex flex-shrink-1 flex-col gap-3 p-2 rounded-2xl border transition-border ease-in-out duration-300 bg-[var(--bg-container)]
|
||||
border-[var(--color-border)] focus-within:border-[var(--color-border-active)]">
|
||||
<div v-if="files.length > 0" class="flex-1 flex gap-2 pt-2 px-2 pb-1 overflow-x-auto flex-wrap">
|
||||
<div v-if="files.length > 0"
|
||||
class="flex-shrink-0 flex flex-nowrap gap-2 pt-2 px-2 pb-1 overflow-x-auto overflow-y-hidden [scrollbar-width:thin]">
|
||||
<!-- TODO: show attachment previews -->
|
||||
<AttachmentPreview v-for="file in files" @delete="files = files.filter((f) => f.id !== file.id)"
|
||||
:key="file.id" :file="file" />
|
||||
@retry="fileSelectorRef?.retryFile(file.id)" :key="file.id" :file="file"></AttachmentPreview>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 max-h-full">
|
||||
<div class="min-w-0 max-h-full">
|
||||
<!-- Grammarly literally breaks everything, go fuck yourself -->
|
||||
<!-- It is absolutely paramount that the closing tag for the textare has ZERO whitespace between the end of the textarea opening tag, otherwise there will be hydration errors -->
|
||||
<textarea data-gramm="false" id="chat" v-model="textAreaValue" ref="inputRef"
|
||||
@@ -336,20 +430,77 @@ onUnmounted(() => {
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<!-- TODO: since we dont want to model selector dropdown to potentially overflow, it has max-width: 100%, so, we need to maake the trigger large enough to fit the entire width of the dropdown -->
|
||||
<div class="flex flex-1 gap-1 min-w-0">
|
||||
<div class="flex flex-1 items-center gap-1 min-w-0">
|
||||
<button v-if="selectedModel?.capabilities.includes('tools')"
|
||||
type="button"
|
||||
@click="isDrawerOpen = !isDrawerOpen"
|
||||
class="sm:hidden relative flex items-center justify-center h-8.5 w-8.5 shrink-0 @hover:bg-[var(--color-hover)] rounded-lg transition-colors duration-200"
|
||||
:aria-label="isDrawerOpen ? 'Close tools drawer' : 'Open tools drawer'"
|
||||
:aria-expanded="isDrawerOpen">
|
||||
<span class="i-mynaui-chevron-down text-5 text-[var(--text-secondary)] transition-transform duration-200"
|
||||
:class="{ 'rotate-180': isDrawerOpen }"></span>
|
||||
<span v-if="!isDrawerOpen && (searchConfig.enabled || toolsConfig.python)"
|
||||
class="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-[var(--color-accent)]"></span>
|
||||
</button>
|
||||
<ModelSelector v-if="providers !== undefined" :add-hotkey="true" v-model="selectedModel"
|
||||
:providers="providers" />
|
||||
<SearchSelector v-if="selectedModel?.capabilities.includes('tools')" :enabled="searchConfig.enabled"
|
||||
:max-results="searchConfig.maxResults" :rerank="searchConfig.rerank"
|
||||
@update:enabled="(v: boolean) => { searchConfig.enabled = v; saveSearchConfig() }"
|
||||
@update:max-results="(v: number) => { searchConfig.maxResults = v; saveSearchConfig() }"
|
||||
@update:rerank="(v: boolean) => { searchConfig.rerank = v; saveSearchConfig() }" />
|
||||
<FileSelector ref="fileSelectorRef" :selected-model="selectedModel" v-model="files" />
|
||||
:providers="providers"></ModelSelector>
|
||||
<div v-if="selectedModel?.capabilities.includes('tools')" class="hidden sm:flex items-center gap-1">
|
||||
<SearchSelector :enabled="searchConfig.enabled"
|
||||
@update:enabled="(v: boolean) => { searchConfig.enabled = v; saveSearchConfig(); }"></SearchSelector>
|
||||
<ToolSelector :python="toolsConfig.python"
|
||||
@update:python="(v: boolean) => { toolsConfig.python = v; saveToolsConfig(); }"></ToolSelector>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<FileSelector ref="fileSelectorRef" :selected-model="selectedModel" v-model="files"></FileSelector>
|
||||
<div v-if="allowManualRole" class="flex items-center">
|
||||
<button aria-label="Send message" @click="handleSubmit"
|
||||
:disabled="(!inputValue.content.trim() && files.length === 0) && !loading" :class="[
|
||||
:disabled="((!inputValue.content.trim() && files.length === 0) && !loading) || isUploading || hasFailedUploads"
|
||||
:class="[
|
||||
'h-8 w-8 rounded-l-lg rounded-r-none transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed disabled:bg-transparent',
|
||||
(inputValue.content.trim() || files.length > 0) && !loading && !isUploading && !hasFailedUploads
|
||||
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)]'
|
||||
: 'text-[var(--text-dim)]',
|
||||
loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]',
|
||||
]">
|
||||
<span v-if="loading" class="text-6.5 i-mynaui-stop-solid"></span>
|
||||
<span v-else class="text-5 i-mynaui-send-solid"></span>
|
||||
</button>
|
||||
<Dropdown placement="bottom-end">
|
||||
<template #default="{ setRef, isOpen, toggle }">
|
||||
<button :ref="setRef" aria-label="Message options" @click="toggle"
|
||||
:disabled="(!inputValue.content.trim() && files.length === 0) || loading || isUploading || hasFailedUploads"
|
||||
:class="[
|
||||
'h-8 w-6 rounded-r-lg rounded-l-none transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed border-l border-[var(--text-dim)] disabled:border-transparent',
|
||||
(inputValue.content.trim() || files.length > 0) && !loading && !isUploading && !hasFailedUploads
|
||||
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)] disabled:bg-transparent'
|
||||
: 'text-[var(--text-dim)]',
|
||||
loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]',
|
||||
]">
|
||||
<span class="text-4.5 i-mynaui-chevron-down" :class="{ 'rotate-180': isOpen }"></span>
|
||||
</button>
|
||||
</template>
|
||||
<template #dropdown="{ close }">
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm text-[var(--text-primary)] @hover:bg-[var(--color-hover)] transition-colors whitespace-nowrap"
|
||||
@click="handleAddMessage('user'); close()">
|
||||
<span class="text-4 i-mynaui-user"></span>
|
||||
Add as User
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm text-[var(--text-primary)] @hover:bg-[var(--color-hover)] transition-colors whitespace-nowrap"
|
||||
@click="handleAddMessage('assistant'); close()">
|
||||
<span class="text-4 i-mynaui-sparkles"></span>
|
||||
Add as Assistant
|
||||
</button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<button v-else aria-label="Send message" @click="handleSubmit"
|
||||
:disabled="((!inputValue.content.trim() && files.length === 0) && !loading) || isUploading || hasFailedUploads"
|
||||
:class="[
|
||||
'h-8 w-8 rounded-xl transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed disabled:bg-transparent',
|
||||
(inputValue.content.trim() || files.length > 0) && !loading
|
||||
(inputValue.content.trim() || files.length > 0) && !loading && !isUploading && !hasFailedUploads
|
||||
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)]'
|
||||
: 'text-[var(--text-dim)]',
|
||||
loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]',
|
||||
@@ -360,4 +511,17 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedModel?.capabilities.includes('tools')"
|
||||
class="sm:hidden relative z-0 mx-auto w-[calc(100%-4px)] transition-[height] duration-250 ease-[cubic-bezier(0.4,_0,_0.2,_1)]"
|
||||
:class="isDrawerOpen ? 'h-[44px]' : 'h-0'">
|
||||
<div class="absolute -top-4 left-0 right-0 pt-5 px-3 pb-1.5 rounded-b-2xl border-x border-b border-[var(--color-border)] bg-[var(--bg-surface)] flex items-center gap-1.5 transition-[transform,opacity] duration-250 ease-[cubic-bezier(0.4,_0,_0.2,_1)]"
|
||||
:class="isDrawerOpen ? 'translate-y-0 opacity-100' : '-translate-y-[44px] opacity-0 pointer-events-none'">
|
||||
<SearchSelector :enabled="searchConfig.enabled"
|
||||
@update:enabled="(v: boolean) => { searchConfig.enabled = v; saveSearchConfig(); }"></SearchSelector>
|
||||
<ToolSelector :python="toolsConfig.python"
|
||||
@update:python="(v: boolean) => { toolsConfig.python = v; saveToolsConfig(); }"></ToolSelector>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_BASE_DELAY_MS = 1000;
|
||||
|
||||
const props = defineProps<{
|
||||
selectedModel?: ModelWithProvider | null;
|
||||
}>();
|
||||
@@ -15,10 +18,12 @@ const files = defineModel<{
|
||||
status: 'uploading' | 'uploaded' | 'error';
|
||||
url: string;
|
||||
progress: number;
|
||||
retryCount: number;
|
||||
}[]>({ required: false, default: [] });
|
||||
const rawFiles = ref<File[]>([]);
|
||||
|
||||
const activeUploads = ref<Map<string, XMLHttpRequest>>(new Map());
|
||||
const pendingFiles = ref<Map<string, File>>(new Map());
|
||||
|
||||
const uploadWithProgress = (file: File, url: string, id: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -39,12 +44,16 @@ const uploadWithProgress = (file: File, url: string, id: string) => {
|
||||
|
||||
xhr.onload = () => {
|
||||
activeUploads.value.delete(id);
|
||||
if (xhr.status >= 400) {
|
||||
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||
} else {
|
||||
resolve(xhr);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
activeUploads.value.delete(id);
|
||||
reject(xhr);
|
||||
reject(new Error('Network error during upload'));
|
||||
};
|
||||
|
||||
xhr.onabort = () => {
|
||||
@@ -57,21 +66,9 @@ const uploadWithProgress = (file: File, url: string, id: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
try {
|
||||
const id = nanoid();
|
||||
const fileName = file.name || `${id}.png`;
|
||||
const fileType = file.type || 'application/octet-stream';
|
||||
|
||||
files.value.push({
|
||||
id,
|
||||
name: fileName,
|
||||
mimeType: fileType,
|
||||
status: 'uploading',
|
||||
url: URL.createObjectURL(file),
|
||||
progress: 0
|
||||
});
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const uploadFileAttempt = async (file: File, id: string, fileName: string, fileType: string) => {
|
||||
const { url: uploadUrl, assetUrl } = await $fetch('/api/upload/presigned', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
@@ -93,32 +90,98 @@ const uploadFile = async (file: File) => {
|
||||
size: file.size,
|
||||
url: assetUrl,
|
||||
},
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
const id = nanoid();
|
||||
const fileName = file.name || `${id}.png`;
|
||||
const fileType = file.type || 'application/octet-stream';
|
||||
|
||||
files.value.push({
|
||||
id,
|
||||
name: fileName,
|
||||
mimeType: fileType,
|
||||
status: 'uploading',
|
||||
url: URL.createObjectURL(file),
|
||||
progress: 0,
|
||||
retryCount: 0,
|
||||
});
|
||||
pendingFiles.value.set(id, file);
|
||||
|
||||
await attemptUpload(file, id, fileName, fileType);
|
||||
};
|
||||
|
||||
const attemptUpload = async (file: File, id: string, fileName: string, fileType: string, attempt = 0) => {
|
||||
try {
|
||||
await uploadFileAttempt(file, id, fileName, fileType);
|
||||
|
||||
files.value = files.value.map(f => {
|
||||
if (f.name === file.name) {
|
||||
return {
|
||||
...f,
|
||||
status: 'uploaded',
|
||||
progress: 100
|
||||
};
|
||||
if (f.id === id) {
|
||||
return { ...f, status: 'uploaded' as const, progress: 100 };
|
||||
}
|
||||
return f;
|
||||
});
|
||||
pendingFiles.value.delete(id);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Upload aborted') {
|
||||
pendingFiles.value.delete(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextAttempt = attempt + 1;
|
||||
|
||||
if (nextAttempt < MAX_RETRIES) {
|
||||
files.value = files.value.map(f => {
|
||||
if (f.name === file.name) {
|
||||
return {
|
||||
...f,
|
||||
status: 'error'
|
||||
};
|
||||
if (f.id === id) {
|
||||
return { ...f, retryCount: nextAttempt, progress: 0 };
|
||||
}
|
||||
return f;
|
||||
});
|
||||
|
||||
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
|
||||
await sleep(delay);
|
||||
|
||||
const stillExists = files.value.some(f => f.id === id);
|
||||
if (!stillExists) return;
|
||||
|
||||
files.value = files.value.map(f => {
|
||||
if (f.id === id) {
|
||||
return { ...f, status: 'uploading' as const };
|
||||
}
|
||||
return f;
|
||||
});
|
||||
|
||||
await attemptUpload(file, id, fileName, fileType, nextAttempt);
|
||||
} else {
|
||||
files.value = files.value.map(f => {
|
||||
if (f.id === id) {
|
||||
return { ...f, status: 'error' as const };
|
||||
}
|
||||
return f;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ uploadFile });
|
||||
const retryFile = async (id: string) => {
|
||||
const file = pendingFiles.value.get(id);
|
||||
if (!file) return;
|
||||
|
||||
const entry = files.value.find(f => f.id === id);
|
||||
if (!entry) return;
|
||||
|
||||
files.value = files.value.map(f => {
|
||||
if (f.id === id) {
|
||||
return { ...f, status: 'uploading' as const, progress: 0, retryCount: 0 };
|
||||
}
|
||||
return f;
|
||||
});
|
||||
|
||||
await attemptUpload(file, id, entry.name, entry.mimeType);
|
||||
};
|
||||
|
||||
defineExpose({ uploadFile, retryFile });
|
||||
|
||||
watch(rawFiles, async (newFiles, oldFiles) => {
|
||||
const diff = newFiles.filter(f =>
|
||||
@@ -146,6 +209,8 @@ watch(files, async (newFiles, oldFiles) => {
|
||||
activeUploads.value.delete(removedFile.id);
|
||||
}
|
||||
|
||||
pendingFiles.value.delete(removedFile.id);
|
||||
|
||||
if (removedFile.url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(removedFile.url);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { useId } from "vue";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>(), {
|
||||
size: 24,
|
||||
});
|
||||
|
||||
const titleId = useId();
|
||||
const BACKGROUND_COLOR = "#a3e6d9";
|
||||
const AVATAR_SCALE = 0.7;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="{ width: `${size}px`, height: `${size}px`, backgroundColor: avatar ? BACKGROUND_COLOR : undefined, borderRadius: avatar ? '0.375rem' : undefined }">
|
||||
<!-- Artwork from https://github.com/lobehub/lobe-icons/pull/396 (MIT; see LICENSE). -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size"
|
||||
viewBox="0 0 24 24" :fill="avatar ? '#000' : 'currentColor'" fill-rule="evenodd"
|
||||
role="img" :aria-labelledby="titleId" style="flex: none; line-height: 1;"
|
||||
:style="{ transform: avatar ? `scale(${AVATAR_SCALE})` : undefined }">
|
||||
<title :id="titleId">Dots Studio</title>
|
||||
<path d="M8.429 23.18a67 67 0 0 1-.423-2.074c0-.024 2.298-.042 5.106-.042 4.836 0 5.104.005 5.077.098a19 19 0 0 0-.148.714c-.066.34-.158.787-.204.996l-.084.378h-4.651c-3.671 0-4.656-.015-4.673-.07m1.89-6.37-6.351-1.1-.054-.195c-.05-.177-.025-.247.286-.836 1.975-3.727 5.2-7.438 8.824-10.153 1.898-1.422 4.002-2.647 6.013-3.502.733-.311.744-.314.897-.218l.17.106c.01.005-.728 3.81-1.64 8.457-.91 4.645-1.655 8.47-1.655 8.5s-.032.052-.07.048-2.928-.502-6.42-1.107m4.799-1.675c.293-1.376 2.09-10.679 2.067-10.701-.017-.017-.354.172-.75.418-2.186 1.362-3.772 2.637-5.599 4.501a27.4 27.4 0 0 0-3.302 4.03c-.367.545-.419.648-.331.668.25.056 7.628 1.337 7.737 1.343.101.006.13-.037.178-.26z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { useId } from "vue";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>(), {
|
||||
size: 24,
|
||||
});
|
||||
|
||||
const titleId = useId();
|
||||
const BACKGROUND_COLOR = "#fff";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="{ width: `${size}px`, height: `${size}px`, backgroundColor: avatar ? BACKGROUND_COLOR : undefined, borderRadius: avatar ? '0.375rem' : undefined }">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size"
|
||||
viewBox="0 0 24 24" :fill="avatar || color ? '#000' : 'currentColor'" role="img" :aria-labelledby="titleId"
|
||||
style="flex: none; line-height: 1;"
|
||||
:style="{ transform: avatar ? `scale(${AVATAR_SCALE})` : undefined }">
|
||||
<title :id="titleId">Nex AGI</title>
|
||||
<g transform="matrix(.02777778 0 0 .02777778 -2.222222 -2.222222)">
|
||||
<path d="M634.153 529.915C634.153 536.056 631.023 541.773 625.849 545.081C620.674 548.388 614.17 548.83 608.597 546.251L367.982 434.957L367.983 749.839C367.983 755.934 364.899 761.615 359.787 764.935L137.804 909.096C132.269 912.69 125.211 912.968 119.411 909.819C113.611 906.67 110 900.599 110 894V156.359C110 150.25 113.099 144.558 118.229 141.242C123.361 137.926 129.823 137.438 135.394 139.948L623.547 359.872C630.002 362.78 634.153 369.203 634.153 376.284V529.915Z"></path>
|
||||
<path d="M389.847 493.085C389.847 486.944 392.977 481.227 398.151 477.919C403.326 474.612 409.83 474.17 415.403 476.749L656.018 588.043L656.017 273.161C656.017 267.066 659.101 261.385 664.213 258.065L886.196 113.904C891.731 110.31 898.789 110.032 904.589 113.181C910.389 116.33 914 122.401 914 129L914 866.641C914 872.75 910.901 878.442 905.771 881.758C900.639 885.074 894.177 885.562 888.606 883.052L400.453 663.128C393.998 660.22 389.847 653.797 389.847 646.716V493.085Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,17 +16,16 @@ const [a, b, c, d] = useFillIds(TITLE, 4);
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<!-- TODO: scale to 24px -->
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
style="flex: none; line-height: 1;" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
style="flex: none; line-height: 1;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<mask :id="d!.id" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="32">
|
||||
<rect width="32" height="32" :fill="a!.fill" style="" />
|
||||
<rect width="32" height="32" :fill="b!.fill" style="" />
|
||||
<rect width="32" height="32" :fill="c!.fill" style="" />
|
||||
</mask>
|
||||
<g :mask="d!.fill">
|
||||
<path
|
||||
<g transform="scale(0.75)">
|
||||
<path :mask="d!.fill"
|
||||
d="M8.98976 30.3815C6.02045 28.9329 3.60852 26.6447 2.01266 23.7643C0.453682 20.9498 -0.219858 17.775 0.0629571 14.5827C0.126988 13.8674 0.757216 13.3392 1.47289 13.4024C2.18722 13.466 2.71622 14.0968 2.65309 14.8126C2.41613 17.4865 2.98128 20.1467 4.28757 22.5053C5.41219 24.536 7.023 26.2153 8.9891 27.4192L15.1043 14.8784C12.7106 14.0812 10.7169 14.4282 10.5375 14.4631C10.5112 14.4692 10.4858 14.4735 10.4599 14.4786C9.86386 14.5784 9.28735 14.2527 9.05527 13.7111C8.73499 13.1131 7.77341 11.6515 6.60959 11.0837C5.44577 10.516 3.65213 10.6991 3.07471 10.8246C2.58789 10.931 2.08122 10.7494 1.77371 10.358C1.4662 9.9665 1.40726 9.43259 1.62554 8.98496C5.48998 1.05529 15.087 -2.24965 23.0153 1.61816C30.9437 5.48598 34.2453 15.0756 30.3875 23.003C30.3836 23.0111 30.3796 23.0192 30.3752 23.0282C26.504 30.949 16.9145 34.2476 8.98976 30.3815ZM17.44 16.0179L11.3253 28.5578C17.4968 30.8631 24.5196 28.3019 27.731 22.4803C27.2796 21.7817 26.4875 20.78 25.5709 20.3328C24.3864 19.7549 22.6509 19.9442 22.0547 20.0695C21.9566 20.0928 21.8588 20.1041 21.7604 20.105C21.5842 20.1059 21.4054 20.071 21.2339 19.9962C21.056 19.9183 20.8952 19.8009 20.7654 19.6497C20.6912 19.5623 20.6299 19.4667 20.5806 19.3648C20.5422 19.2882 19.5939 17.4481 17.4391 16.0175L17.44 16.0179ZM7.74737 8.74582C8.74313 9.2316 9.5548 10.0037 10.1608 10.7321C12.0348 7.86545 14.578 5.70023 16.5425 4.3042C17.2726 3.78579 18.0284 3.30111 18.7563 2.88291C13.6429 1.80923 8.28375 3.83577 5.1764 8.10245C5.99889 8.15876 6.90528 8.33501 7.74737 8.74582ZM24.6415 5.75507C24.7602 6.58628 24.8441 7.47954 24.8847 8.37503C24.9943 10.7762 24.8555 14.1022 23.7588 17.3369C24.6735 17.3614 25.7372 17.5232 26.7105 17.998C27.5795 18.4219 28.3081 19.0633 28.8814 19.7035C30.3453 14.6184 28.6441 9.13194 24.6411 5.75597L24.6415 5.75507ZM17.4489 13.0336C19.2472 13.9109 20.5471 15.078 21.4282 16.0842C22.8614 11.427 22.2366 6.21527 21.7149 4.28506C19.873 5.06221 15.3815 7.77795 12.5957 11.7753C13.9322 11.8499 15.6515 12.1568 17.4489 13.0336Z"
|
||||
:fill="!avatar && color ? '#4137FF' : 'currentColor'" style="fill-opacity:1;" />
|
||||
</g>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { useId } from "vue";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>(), {
|
||||
size: 24,
|
||||
});
|
||||
|
||||
const titleId = useId();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="{ width: `${size}px`, height: `${size}px`, borderRadius: avatar ? '0.375rem' : undefined, overflow: avatar ? 'hidden' : undefined }">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size"
|
||||
viewBox="0 0 24 24" role="img" :aria-labelledby="titleId"
|
||||
style="flex: none; line-height: 1;">
|
||||
<title :id="titleId">Thinking Machines Lab</title>
|
||||
<rect width="24" height="24" rx="2.6666666666666665"
|
||||
:fill="color ? '#e6e7e8' : 'currentColor'" :fill-opacity="color ? 1 : 0.15"></rect>
|
||||
<rect x="4" y="4" width="16" height="16" rx="1.3333333333333333"
|
||||
:fill="color ? '#31373d' : 'currentColor'"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { renderMath } from 'comark/plugins/math';
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
display: boolean;
|
||||
}>();
|
||||
|
||||
const html = computed(() => renderMath(props.content, props.display, { throwOnError: false }));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="!display" class="math-inline" v-html="html"></span>
|
||||
<div v-else class="math-block" v-html="html"></div>
|
||||
</template>
|
||||
@@ -1,103 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { h, Text, computed } from 'vue';
|
||||
import type { MarkdownAstNode } from '~/utils/markdown';
|
||||
import { agentParser, userParser, getNodeAttributes, getNodeChildren, getNodeTag, splitParagraphBreaks, textContent, toVueAttributes } from '~/utils/markdown';
|
||||
import { createTextVNode, h, shallowRef, watch } from 'vue';
|
||||
import type { SearchSource } from '~/utils/tool-rendering';
|
||||
import SourceCitation from './SourceCitation.vue';
|
||||
import MarkdownMath from './Math.vue';
|
||||
import MarkdownShikiHighlight from './ShikiHighlight.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
finished: boolean;
|
||||
id: string;
|
||||
sources?: ReadonlyMap<string, SearchSource>;
|
||||
citations?: boolean;
|
||||
}>();
|
||||
|
||||
const { $remark } = useNuxtApp();
|
||||
const parser = props.citations ? agentParser : userParser;
|
||||
const parse = async (content: string, finished: boolean) => {
|
||||
const tree = await parser(content, { streaming: !finished });
|
||||
// Copy-on-write: br-free trees return by reference with no allocation.
|
||||
return splitParagraphBreaks(tree);
|
||||
};
|
||||
|
||||
// this function effectively removes lazy concetenation from the markdown
|
||||
// normally in markdown, if you have two lines that are separated by only
|
||||
// one new line. E.g.:
|
||||
//
|
||||
// This is some markdown text but I split
|
||||
// it into two lines so it's easier to read in the editor
|
||||
//
|
||||
// they are *usually* concatenated into one line, however,
|
||||
// since we are a web editor, with line wrapping, that behavior
|
||||
// in undesirable, so pre preserve newlines by just maiking
|
||||
// every new line two newlines (as long as we arent in a codeblock)
|
||||
// function preprocessMarkdown(doc: string) {
|
||||
// const lines = doc.split('\n');
|
||||
// let inCode = false;
|
||||
// let result: string[] = [];
|
||||
// Parsed trees are replaced, never mutated. Avoid proxying every AST node.
|
||||
const parsed = shallowRef(await parse(props.content, props.finished));
|
||||
let parseVersion = 0;
|
||||
|
||||
// for (let i = 0; i < lines.length; i++) {
|
||||
// const fullLine = lines[i]!;
|
||||
watch(
|
||||
[() => props.content, () => props.finished],
|
||||
async ([content, finished]) => {
|
||||
const version = ++parseVersion;
|
||||
const tree = await parse(content, finished);
|
||||
if (version === parseVersion) {
|
||||
parsed.value = tree;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// const match = fullLine.match(/^( {0,3})(.*)/);
|
||||
// const content = match?.[2] || '';
|
||||
const renderNode = (node: MarkdownAstNode, path: string): any => {
|
||||
if (typeof node === 'string') {
|
||||
return createTextVNode(node);
|
||||
}
|
||||
|
||||
// if (content.startsWith('```')) {
|
||||
// if (!inCode) {
|
||||
// inCode = true;
|
||||
// } else {
|
||||
// inCode = false;
|
||||
// }
|
||||
// result.push(fullLine);
|
||||
// continue;
|
||||
// }
|
||||
if (!Array.isArray(node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if (inCode) {
|
||||
// result.push(fullLine);
|
||||
// } else {
|
||||
// if (content.trim().length === 0) {
|
||||
// result.push('');
|
||||
// } else {
|
||||
// if (result.length > 0 && result[result.length - 1] !== '') {
|
||||
// result.push('');
|
||||
// }
|
||||
// result.push(fullLine);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if (node[0] === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// return result.join('\n');
|
||||
// }
|
||||
const tag = getNodeTag(node);
|
||||
const attributes = getNodeAttributes(node);
|
||||
if (tag === 'source-cite') {
|
||||
const ids = typeof attributes.ids === "string" ? attributes.ids.split(",") : [];
|
||||
return h(SourceCitation, { key: path, ids, sources: props.sources });
|
||||
}
|
||||
|
||||
|
||||
const ast = computed(() => {
|
||||
const mdast = $remark.parse(props.content);
|
||||
return $remark.runSync(mdast);
|
||||
});
|
||||
|
||||
const renderNode = (node: any, index: number): any => {
|
||||
if (node.type === 'text' || node.type === 'raw') return h(Text, node.value);
|
||||
|
||||
if (node.type === 'element') {
|
||||
if (node.tagName === 'code') {
|
||||
const isBlock = node.position?.start.line !== node.position?.end.line;
|
||||
if (isBlock && node.children?.[0]?.type === 'text') {
|
||||
if (tag === 'pre') {
|
||||
const language = typeof attributes.language === 'string' ? attributes.language : 'text';
|
||||
return h(MarkdownShikiHighlight, {
|
||||
key: `code-${index}`,
|
||||
code: node.children[0].value,
|
||||
language: node.properties?.className?.[0]?.replace('language-', '') || 'text'
|
||||
key: path,
|
||||
code: textContent(node),
|
||||
language,
|
||||
});
|
||||
}
|
||||
|
||||
if (tag === 'math') {
|
||||
return h(MarkdownMath, {
|
||||
key: path,
|
||||
content: typeof attributes.content === 'string' ? attributes.content : textContent(node),
|
||||
display: typeof attributes.class === 'string' && attributes.class.includes('block'),
|
||||
});
|
||||
}
|
||||
|
||||
const children = node.children?.map((child: any, i: number) => renderNode(child, i)) || [];
|
||||
const children = getNodeChildren(node)
|
||||
.map((child, index) => renderNode(child, `${path}.${index}`))
|
||||
.filter((child) => child !== null);
|
||||
|
||||
return h(
|
||||
node.tagName,
|
||||
{ ...node.properties, key: `${node.tagName}-${index}` },
|
||||
children
|
||||
tag,
|
||||
{ ...toVueAttributes(attributes), key: path },
|
||||
children,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
const children = ast.value?.children?.flatMap(renderNode) || [];
|
||||
const children = parsed.value.nodes
|
||||
.map((node, index) => renderNode(node, `${props.id}.${index}`))
|
||||
.filter((child) => child !== null);
|
||||
|
||||
return h('div', { class: 'prose-wrapper' }, [
|
||||
h('article', { class: 'markdown-body' }, children)
|
||||
]);
|
||||
}
|
||||
return h('article', { class: 'prose-wrapper markdown-body' }, children);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -3,8 +3,6 @@ import { type Grammar } from 'shiki';
|
||||
import { hashSync } from '~/utils/hash';
|
||||
const props = defineProps<{ code: string; language: string }>();
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const MAX_LENGTH = 15000;
|
||||
|
||||
const renderId = `${useId()}-${hashSync(props.code + props.language)}`;
|
||||
@@ -76,8 +74,6 @@ onUnmounted(() => {
|
||||
if (copyTimeout) clearTimeout(copyTimeout);
|
||||
clear();
|
||||
});
|
||||
|
||||
console.log("shiki codeblock rendered in", Date.now() - start);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchSource } from "~/utils/tool-rendering";
|
||||
import { autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/vue";
|
||||
import { computed, onBeforeUnmount, ref, useId } from "vue";
|
||||
import { getFaviconUrl } from "~/utils/tool-rendering";
|
||||
|
||||
const props = defineProps<{
|
||||
ids: string[];
|
||||
sources?: ReadonlyMap<string, SearchSource>;
|
||||
}>();
|
||||
|
||||
const entries = computed(() => [...new Set(props.ids)].map(id => ({ id, source: props.sources?.get(id) })));
|
||||
const firstSource = computed(() => entries.value.find(entry => entry.source)?.source);
|
||||
const label = computed(() => {
|
||||
const first = firstSource.value?.hostname ?? "Source unavailable";
|
||||
return entries.value.length > 1 ? `${first} +${entries.value.length - 1}` : first;
|
||||
});
|
||||
const popupId = useId();
|
||||
const trigger = ref<HTMLElement | null>(null);
|
||||
const preview = ref<HTMLElement | null>(null);
|
||||
const open = ref(false);
|
||||
const failedFavicons = ref(new Set<string>());
|
||||
const { floatingStyles, placement } = useFloating(trigger, preview, {
|
||||
open,
|
||||
strategy: "fixed",
|
||||
placement: "top-start",
|
||||
middleware: [offset(8), flip(), shift({ padding: 12 })],
|
||||
whileElementsMounted: autoUpdate,
|
||||
transform: false,
|
||||
});
|
||||
|
||||
const transformOrigin = computed(() =>
|
||||
placement.value.startsWith("top")
|
||||
? "transform-origin-bottom-center"
|
||||
: "transform-origin-top-center"
|
||||
);
|
||||
|
||||
let closeTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let pointerInside = false;
|
||||
|
||||
const cancelClose = () => {
|
||||
clearTimeout(closeTimer);
|
||||
closeTimer = undefined;
|
||||
};
|
||||
|
||||
const showPreview = () => {
|
||||
cancelClose();
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const scheduleClose = () => {
|
||||
cancelClose();
|
||||
// Allow the pointer to cross the gap between the badge and the preview.
|
||||
closeTimer = setTimeout(() => {
|
||||
closeTimer = undefined;
|
||||
const focused = document.activeElement;
|
||||
if (!pointerInside && !trigger.value?.contains(focused) && !preview.value?.contains(focused)) {
|
||||
open.value = false;
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const onPointerEnter = (event: PointerEvent) => {
|
||||
if (event.pointerType === "touch") {
|
||||
return;
|
||||
}
|
||||
pointerInside = true;
|
||||
showPreview();
|
||||
};
|
||||
|
||||
const onPointerLeave = (event: PointerEvent) => {
|
||||
if (event.pointerType === "touch") {
|
||||
return;
|
||||
}
|
||||
pointerInside = false;
|
||||
scheduleClose();
|
||||
};
|
||||
|
||||
const dismissPreview = () => {
|
||||
cancelClose();
|
||||
if (preview.value?.contains(document.activeElement)) {
|
||||
trigger.value?.focus();
|
||||
}
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
onBeforeUnmount(cancelClose);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a ref="trigger" :href="firstSource?.url" target="_blank" rel="noopener noreferrer"
|
||||
:tabindex="firstSource ? undefined : 0" :aria-expanded="open" :aria-controls="open ? popupId : undefined"
|
||||
@pointerenter="onPointerEnter" @pointerleave="onPointerLeave" @focusin="showPreview" @focusout="scheduleClose"
|
||||
@keydown.esc.stop.prevent="dismissPreview"
|
||||
class="inline-flex items-center gap-1 max-w-64 mx-1 px-2 border border-[var(--color-border)] rounded-full bg-[var(--bg-container)] hover:bg-[var(--color-hover)] transition-colors text-[var(--text-secondary)] text-xs align-baseline cursor-pointer focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-[var(--text-secondary)] focus-visible:outline-offset-2 decoration-none">
|
||||
<img v-if="firstSource && !failedFavicons.has(firstSource.hostname)" :src="getFaviconUrl(firstSource.hostname)"
|
||||
alt="" loading="lazy" class="rounded-[4px] w-[0.9rem] h-[0.9rem] m-0"
|
||||
@error="failedFavicons.add(firstSource.hostname)">
|
||||
{{ label }}
|
||||
</a>
|
||||
<Transition enter-active-class="transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
enter-from-class="opacity-0 scale-95 translate-y-1" enter-to-class="opacity-100 scale-100 translate-y-0"
|
||||
leave-active-class="transition-[opacity,transform] duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
leave-from-class="opacity-100 scale-100 translate-y-0" leave-to-class="opacity-0 scale-95 translate-y-1">
|
||||
<span v-if="open" :id="popupId" ref="preview" @pointerenter="onPointerEnter" @pointerleave="onPointerLeave"
|
||||
@focusin="showPreview" @focusout="scheduleClose" @keydown.esc.stop.prevent="dismissPreview"
|
||||
class="z-2 inset-auto m-0 p-[0.85rem] w-[min(22rem,calc(100vw_-_24px))] max-h-[min(24rem,70vh)] overflow-y-auto border border-[var(--color-border)] rounded-xl bg-[var(--bg-surface)] text-[var(--text-primary)] shadow-md text-sm leading-normal"
|
||||
:class="transformOrigin" :style="floatingStyles">
|
||||
<span v-for="(entry, index) in entries" :key="entry.id"
|
||||
class="flex flex-col gap-[0.4rem] [overflow-wrap:anywhere]"
|
||||
:class="{ 'mt-[0.8rem] pt-[0.8rem] border-t border-[var(--color-border)]': index > 0 }">
|
||||
<template v-if="entry.source">
|
||||
<span class="text-xs text-[var(--text-secondary)]">{{ entry.source.hostname }} · Search
|
||||
excerpt</span>
|
||||
<a :href="entry.source.url" target="_blank" rel="noopener noreferrer">{{ entry.source.title }}</a>
|
||||
<span v-if="entry.source.snippet" class="whitespace-pre-wrap text-[var(--text-secondary)]">{{
|
||||
entry.source.snippet }}</span>
|
||||
</template>
|
||||
<span v-else>Source unavailable ({{ entry.id }})</span>
|
||||
</span>
|
||||
</span>
|
||||
</Transition>
|
||||
</template>
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { MessagePart } from '~/composables/useChat';
|
||||
import ReasoningBody from './ReasoningBody.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
part: Readonly<MessagePart>;
|
||||
}>();
|
||||
|
||||
const reasoningOpen = ref(!props.part.finished);
|
||||
const bodyRef = ref<InstanceType<typeof ReasoningBody> | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.part.finished,
|
||||
@@ -14,44 +16,14 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
const containerRef: Ref<HTMLDivElement | null> = ref(null);
|
||||
const scrollState = ref('middle');
|
||||
|
||||
const { scrollToBottom } = useAutoScroll(containerRef);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
const container = containerRef.value;
|
||||
const scrollTop = container.scrollTop;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const clientHeight = container.clientHeight;
|
||||
|
||||
const topThreshold = 0.02 * clientHeight;
|
||||
|
||||
if (scrollHeight <= clientHeight) {
|
||||
// the container does not have enough content to scroll
|
||||
scrollState.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollTop <= topThreshold) {
|
||||
scrollState.value = 'top';
|
||||
} else if (scrollTop + 100 >= scrollHeight - clientHeight) {
|
||||
scrollState.value = 'bottom';
|
||||
} else {
|
||||
scrollState.value = 'middle';
|
||||
}
|
||||
};
|
||||
|
||||
const toggleReasoning = async () => {
|
||||
if (!props.part.finished) return;
|
||||
|
||||
reasoningOpen.value = !reasoningOpen.value;
|
||||
if (!reasoningOpen.value) return;
|
||||
await nextTick();
|
||||
scrollToBottom('instant');
|
||||
handleScroll();
|
||||
bodyRef.value?.scrollToBottom('instant');
|
||||
bodyRef.value?.handleScroll();
|
||||
};
|
||||
|
||||
const formatDuration = (ms: number) => {
|
||||
@@ -91,11 +63,6 @@ const formatDuration = (ms: number) => {
|
||||
<span class="i-mynaui-chevron-down w-4 h-4 text-[var(--text-secondary)]"
|
||||
:class="reasoningOpen ? '' : '-rotate-90'"></span>
|
||||
</button>
|
||||
<div v-if="reasoningOpen" ref="containerRef" @scroll="handleScroll"
|
||||
:class="['reasoning-container max-h-[min(40vh,320px)] overflow-y-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]', scrollState]">
|
||||
<div class="p-2">
|
||||
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content!" />
|
||||
</div>
|
||||
</div>
|
||||
<ReasoningBody v-if="reasoningOpen" ref="bodyRef" :part="part" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import type { MessagePart } from '~/composables/useChat';
|
||||
import { nextTick, onMounted, ref, watch, type Ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
part: Readonly<MessagePart>;
|
||||
}>();
|
||||
|
||||
const containerRef: Ref<HTMLDivElement | null> = ref(null);
|
||||
const scrollState = ref('');
|
||||
|
||||
const { scrollToBottom } = useAutoScroll(containerRef);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!containerRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = containerRef.value;
|
||||
const scrollTop = container.scrollTop;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const clientHeight = container.clientHeight;
|
||||
|
||||
if (scrollHeight <= clientHeight) {
|
||||
scrollState.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const threshold = Math.max(6, 0.02 * clientHeight);
|
||||
|
||||
if (scrollTop <= threshold) {
|
||||
scrollState.value = 'top';
|
||||
} else if (scrollTop >= scrollHeight - clientHeight - threshold) {
|
||||
scrollState.value = 'bottom';
|
||||
} else {
|
||||
scrollState.value = 'middle';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
handleScroll();
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.part.content,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
handleScroll();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
defineExpose({ scrollToBottom, handleScroll });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" @scroll="handleScroll"
|
||||
:class="['reasoning-container max-h-[min(40vh,320px)] overflow-y-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]', scrollState]">
|
||||
<div class="p-2">
|
||||
<MarkdownRenderer :finished="props.part.finished" :id="props.part.id" :content="props.part.content!" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MessagePart } from '~/composables/useChat';
|
||||
|
||||
const props = defineProps<{
|
||||
part: Readonly<MessagePart>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content!" />
|
||||
</template>
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useNuxtApp } from "#app";
|
||||
|
||||
const props = defineProps<{
|
||||
code: string;
|
||||
lang?: string;
|
||||
}>();
|
||||
|
||||
const { $shiki } = useNuxtApp();
|
||||
const html = ref("");
|
||||
|
||||
const targetLang = computed(() => props.lang || "json");
|
||||
|
||||
const lineNumberWidth = computed(() => {
|
||||
const lines = props.code ? props.code.split("\n").length : 1;
|
||||
return lines.toString().length;
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.code, targetLang],
|
||||
async ([newCode, newLang]) => {
|
||||
if (!newCode) {
|
||||
html.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
let resolvedLang = newLang;
|
||||
try {
|
||||
if ($shiki?.getLanguage) {
|
||||
await $shiki.getLanguage(resolvedLang);
|
||||
}
|
||||
} catch {
|
||||
resolvedLang = "text";
|
||||
}
|
||||
|
||||
try {
|
||||
if ($shiki?.codeToHtml) {
|
||||
html.value = await $shiki.codeToHtml(newCode, {
|
||||
lang: resolvedLang,
|
||||
themes: {
|
||||
dark: "vitesse-dark",
|
||||
light: "vitesse-light",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
html.value = "";
|
||||
}
|
||||
} catch {
|
||||
html.value = "";
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="tool-code-container overflow-auto max-h-80 scrollbar-thin text-xs font-mono"
|
||||
:style="`--line-number-width: ${lineNumberWidth}ch;`"
|
||||
>
|
||||
<div v-if="html" class="tool-code-highlight" v-html="html"></div>
|
||||
<pre v-else class="tool-code-fallback m-0 p-3.5 text-[var(--text-secondary)] whitespace-pre-wrap break-words leading-relaxed">{{ code }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tool-code-container {
|
||||
background-color: var(--shiki-dark-bg, #121019);
|
||||
}
|
||||
|
||||
:deep(.tool-code-highlight > pre) {
|
||||
margin: 0;
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.625;
|
||||
scrollbar-width: thin;
|
||||
overflow-x: auto;
|
||||
counter-reset: lines;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
:deep(.tool-code-highlight > pre > code .line::before) {
|
||||
counter-increment: lines;
|
||||
content: counter(lines);
|
||||
width: var(--line-number-width, 2ch);
|
||||
margin-right: 1.25rem;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:deep(html.light .tool-code-container) {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
:deep(html.light .tool-code-highlight > pre > code .line::before) {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToolCall } from '~/composables/useChat';
|
||||
import { formatToolValue, parseToolValue } from '~/utils/tool-rendering';
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: Readonly<ToolCall>;
|
||||
@@ -28,21 +29,21 @@ const input: ComputedRef<string> = computed(() => {
|
||||
case 'input':
|
||||
if (props.toolCall.input === null || props.toolCall.input === undefined) return '';
|
||||
if (props.toolCall.input!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.input!.value), null, 2);
|
||||
return formatToolValue(props.toolCall.input);
|
||||
}
|
||||
|
||||
return props.toolCall.input!.value;
|
||||
case 'output':
|
||||
if (props.toolCall.output === null || props.toolCall.output === undefined) return '';
|
||||
if (props.toolCall.output!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.output!.value), null, 2);
|
||||
return formatToolValue(props.toolCall.output);
|
||||
}
|
||||
|
||||
return props.toolCall.output!.value;
|
||||
case 'error':
|
||||
if (props.toolCall.error === null || props.toolCall.error === undefined) return '';
|
||||
if (props.toolCall.error!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.error!.value), null, 2);
|
||||
return formatToolValue(props.toolCall.error);
|
||||
}
|
||||
|
||||
return props.toolCall.error!.value;
|
||||
@@ -55,7 +56,7 @@ const input: ComputedRef<string> = computed(() => {
|
||||
traceObj.input = traceObj.input.value;
|
||||
break;
|
||||
case 'json':
|
||||
traceObj.input = JSON.parse(traceObj.input.value);
|
||||
traceObj.input = parseToolValue(props.toolCall.input);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ const input: ComputedRef<string> = computed(() => {
|
||||
traceObj.output = traceObj.output.value;
|
||||
break;
|
||||
case 'json':
|
||||
traceObj.output = JSON.parse(traceObj.output.value);
|
||||
traceObj.output = parseToolValue(props.toolCall.output);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -73,7 +74,7 @@ const input: ComputedRef<string> = computed(() => {
|
||||
traceObj.error = traceObj.error.value;
|
||||
break;
|
||||
case 'json':
|
||||
traceObj.error = JSON.parse(traceObj.error.value);
|
||||
traceObj.error = parseToolValue(props.toolCall.error);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
<script setup lang="ts">
|
||||
import type { MessagePart, ToolCall } from "~/composables/useChat";
|
||||
import { computed, onUnmounted, ref, useId, watch } from "vue";
|
||||
import CodeViewer from "./CodeViewer.vue";
|
||||
import {
|
||||
formatDuration,
|
||||
formatToolValue,
|
||||
getToolDisplayInfo,
|
||||
parseToolValue,
|
||||
} from "~/utils/tool-rendering";
|
||||
|
||||
export interface CustomToolTab {
|
||||
id: string;
|
||||
label: string;
|
||||
count?: string;
|
||||
copyContent?: () => string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: Readonly<ToolCall>;
|
||||
part?: MessagePart;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
icon?: string;
|
||||
customTab?: CustomToolTab;
|
||||
duration?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
retry: [];
|
||||
}>();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const contentId = useId();
|
||||
const copied = ref(false);
|
||||
let copyTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const displayInfo = computed(() => getToolDisplayInfo(props.toolCall));
|
||||
const displayTitle = computed(() => props.title || displayInfo.value.verb);
|
||||
const displaySubtitle = computed(() => props.subtitle !== undefined ? props.subtitle : displayInfo.value.target);
|
||||
const displayIcon = computed(() => props.icon || displayInfo.value.icon);
|
||||
|
||||
const calculatedDuration = computed(() => {
|
||||
if (props.duration) {
|
||||
return props.duration;
|
||||
}
|
||||
if (props.toolCall.status === "pending") {
|
||||
return null;
|
||||
}
|
||||
const start = props.part?.createdAt || props.toolCall.createdAt;
|
||||
const end = props.part?.lastUpdatedAt;
|
||||
return formatDuration(start, end);
|
||||
});
|
||||
|
||||
interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const availableTabs = computed<TabItem[]>(() => {
|
||||
const tabs: TabItem[] = [];
|
||||
|
||||
if (props.customTab) {
|
||||
tabs.push({ id: props.customTab.id, label: props.customTab.label });
|
||||
}
|
||||
|
||||
if (props.toolCall.status === "failed" || props.toolCall.error) {
|
||||
tabs.push({ id: "error", label: "Error" });
|
||||
}
|
||||
|
||||
if (props.toolCall.input) {
|
||||
tabs.push({ id: "input", label: "Input" });
|
||||
}
|
||||
|
||||
if (props.toolCall.output) {
|
||||
tabs.push({ id: "output", label: "Output" });
|
||||
}
|
||||
|
||||
tabs.push({ id: "trace", label: "Trace" });
|
||||
|
||||
return tabs;
|
||||
});
|
||||
|
||||
const resolveDefaultTab = (): string => {
|
||||
if (props.toolCall.status === "failed" || props.toolCall.error) {
|
||||
return "error";
|
||||
}
|
||||
if (props.customTab && props.toolCall.status === "completed") {
|
||||
return props.customTab.id;
|
||||
}
|
||||
if (props.toolCall.output) {
|
||||
return "output";
|
||||
}
|
||||
if (props.toolCall.input) {
|
||||
return "input";
|
||||
}
|
||||
return "trace";
|
||||
};
|
||||
|
||||
const activeTab = ref(resolveDefaultTab());
|
||||
|
||||
watch(
|
||||
() => props.toolCall.status,
|
||||
(newStatus) => {
|
||||
if (newStatus === "failed") {
|
||||
activeTab.value = "error";
|
||||
} else if (newStatus === "completed" && props.customTab) {
|
||||
activeTab.value = props.customTab.id;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const formattedError = computed(() => {
|
||||
if (!props.toolCall.error) {
|
||||
return "";
|
||||
}
|
||||
const parsed = parseToolValue(props.toolCall.error);
|
||||
if (typeof parsed === "string") {
|
||||
return parsed;
|
||||
}
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
});
|
||||
|
||||
const currentTabCode = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case "input":
|
||||
return formatToolValue(props.toolCall.input);
|
||||
case "output":
|
||||
return formatToolValue(props.toolCall.output);
|
||||
case "error":
|
||||
return formattedError.value;
|
||||
case "trace": {
|
||||
const traceObj = {
|
||||
id: props.toolCall.id,
|
||||
toolName: props.toolCall.toolName,
|
||||
status: props.toolCall.status,
|
||||
input: parseToolValue(props.toolCall.input),
|
||||
output: parseToolValue(props.toolCall.output),
|
||||
error: parseToolValue(props.toolCall.error),
|
||||
createdAt: props.toolCall.createdAt,
|
||||
};
|
||||
return JSON.stringify(traceObj, null, 2);
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const currentTabLang = computed(() => {
|
||||
if (activeTab.value === "trace") {
|
||||
return "json";
|
||||
}
|
||||
if (activeTab.value === "input") {
|
||||
return props.toolCall.input?.type === "json" ? "json" : "text";
|
||||
}
|
||||
if (activeTab.value === "output") {
|
||||
return props.toolCall.output?.type === "json" ? "json" : "text";
|
||||
}
|
||||
return "text";
|
||||
});
|
||||
|
||||
const footerInfo = computed(() => {
|
||||
if (props.customTab && activeTab.value === props.customTab.id) {
|
||||
return props.customTab.count || "";
|
||||
}
|
||||
const lines = currentTabCode.value ? currentTabCode.value.split("\n").length : 0;
|
||||
const lineLabel = lines === 1 ? "line" : "lines";
|
||||
const lang = currentTabLang.value;
|
||||
return `${lines} ${lineLabel} · ${lang}`;
|
||||
});
|
||||
|
||||
const canRetry = computed(() => props.toolCall.status === "failed");
|
||||
|
||||
const copyActiveTab = async () => {
|
||||
let textToCopy = "";
|
||||
if (props.customTab && activeTab.value === props.customTab.id && props.customTab.copyContent) {
|
||||
textToCopy = props.customTab.copyContent();
|
||||
} else {
|
||||
textToCopy = currentTabCode.value;
|
||||
}
|
||||
if (!textToCopy) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
copied.value = true;
|
||||
if (copyTimeout) {
|
||||
clearTimeout(copyTimeout);
|
||||
}
|
||||
copyTimeout = setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy tool call content:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (copyTimeout) {
|
||||
clearTimeout(copyTimeout);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tool-disclosure flex flex-col min-w-0 text-[var(--text-tertiary)]">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full @hover:bg-[var(--color-hover)] rounded-lg p-1 flex justify-between items-center transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] cursor-pointer select-none text-left"
|
||||
:aria-expanded="isOpen"
|
||||
:aria-controls="contentId"
|
||||
@click="toggle"
|
||||
>
|
||||
<span class="flex items-center gap-1.5 min-w-0 overflow-hidden">
|
||||
<span class="w-6 h-6 flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center">
|
||||
<span
|
||||
:class="[
|
||||
'w-3 h-3 text-3 transition-colors',
|
||||
displayIcon,
|
||||
toolCall.status === 'failed' ? 'text-red-400' : 'text-[var(--text-secondary)]'
|
||||
]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</span>
|
||||
<span class="text-sm truncate">
|
||||
<span :class="toolCall.status === 'failed' ? 'text-red-400' : 'text-[var(--text-secondary)]'">
|
||||
{{ displayTitle }}
|
||||
</span>
|
||||
<span v-if="displaySubtitle" class="text-[var(--text-tertiary)] ml-1.5 font-normal truncate">
|
||||
{{ displaySubtitle }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="flex items-center gap-2 shrink-0 ml-2">
|
||||
<slot name="summary"></slot>
|
||||
<span
|
||||
v-if="toolCall.status === 'pending'"
|
||||
class="i-svg-spinners-180-ring-with-bg text-3 text-[var(--color-accent)]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span v-else-if="toolCall.status === 'failed'" class="text-xs text-red-400 font-medium">Failed</span>
|
||||
<span v-else-if="toolCall.status === 'cancelled'" class="text-xs text-[var(--text-tertiary)]">Cancelled</span>
|
||||
<span v-else-if="calculatedDuration" class="text-xs text-[var(--text-tertiary)] font-mono">{{ calculatedDuration }}</span>
|
||||
<span
|
||||
class="i-mynaui-chevron-down w-4 h-4 text-[var(--text-secondary)] transition-transform duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
:class="isOpen ? '' : '-rotate-90'"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-show="isOpen"
|
||||
:id="contentId"
|
||||
class="mt-1 border border-[var(--color-border)] rounded-xl bg-[var(--bg-surface)] overflow-hidden text-xs"
|
||||
>
|
||||
<div class="flex items-center gap-1 px-3 pt-2.5 pb-2 overflow-x-auto scrollbar-none border-b border-[var(--color-border)]/40">
|
||||
<button
|
||||
v-for="tab in availableTabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
class="px-2.5 py-1 rounded-md text-xs font-medium transition-colors cursor-pointer shrink-0"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? (tab.id === 'error'
|
||||
? 'bg-red-500/15 text-red-300'
|
||||
: 'bg-[var(--color-hover)] text-[var(--text-primary)]')
|
||||
: 'text-[var(--text-tertiary)] @hover:text-[var(--text-secondary)] @hover:bg-[var(--color-hover)]/50'
|
||||
]"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0">
|
||||
<div v-if="customTab && activeTab === customTab.id">
|
||||
<slot :name="customTab.id">
|
||||
<slot></slot>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'error'">
|
||||
<CodeViewer :code="formattedError" lang="json"></CodeViewer>
|
||||
</div>
|
||||
|
||||
<div v-else-if="['input', 'output', 'trace'].includes(activeTab)">
|
||||
<CodeViewer :code="currentTabCode" :lang="currentTabLang"></CodeViewer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-3 py-2 border-t border-[var(--color-border)]/40 text-xs text-[var(--text-tertiary)] bg-[var(--color-hover)]/20">
|
||||
<div>
|
||||
<span class="font-mono text-[11px] text-[var(--text-tertiary)]">{{ footerInfo }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<slot name="actions"></slot>
|
||||
<button
|
||||
v-if="canRetry"
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded text-[var(--text-tertiary)] @hover:text-red-400 @hover:bg-[var(--color-hover)] transition-colors cursor-pointer text-xs"
|
||||
@click="emit('retry')"
|
||||
>
|
||||
<span class="i-mynaui-refresh w-3.5 h-3.5" aria-hidden="true"></span>
|
||||
<span>Retry</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded text-[var(--text-tertiary)] @hover:text-[var(--text-primary)] @hover:bg-[var(--color-hover)] transition-colors cursor-pointer text-xs"
|
||||
@click="copyActiveTab"
|
||||
>
|
||||
<span :class="copied ? 'i-mynaui-check text-emerald-400' : 'i-mynaui-copy'" class="w-3.5 h-3.5" aria-hidden="true"></span>
|
||||
<span>{{ copied ? 'Copied' : 'Copy' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { MessagePart, ToolCall } from "~/composables/useChat";
|
||||
import Disclosure from "./Disclosure.vue";
|
||||
|
||||
defineProps<{
|
||||
toolCall: Readonly<ToolCall>;
|
||||
part?: MessagePart;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
retry: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Disclosure :tool-call="toolCall" :part="part" @retry="emit('retry')"></Disclosure>
|
||||
</template>
|
||||
@@ -0,0 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
import type { MessagePart, ToolCall } from "~/composables/useChat";
|
||||
import { computed, ref } from "vue";
|
||||
import Disclosure from "./Disclosure.vue";
|
||||
import {
|
||||
domainInitial,
|
||||
getDomainColor,
|
||||
getFaviconUrl,
|
||||
parseToolValue,
|
||||
readSearchSources,
|
||||
readSearchWarnings,
|
||||
} from "~/utils/tool-rendering";
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: Readonly<ToolCall>;
|
||||
part?: MessagePart;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
retry: [];
|
||||
}>();
|
||||
|
||||
const output = computed(() => parseToolValue(props.toolCall.output));
|
||||
const sources = computed(() => readSearchSources(output.value));
|
||||
const warnings = computed(() => readSearchWarnings(output.value));
|
||||
const warningSummary = computed(() => warnings.value.map(warning => `${warning.engine}: ${warning.reason}`).join("; "));
|
||||
|
||||
const query = computed(() => {
|
||||
const input = parseToolValue(props.toolCall.input);
|
||||
if (typeof input === "object" && input !== null && "query" in input && typeof input.query === "string") {
|
||||
return input.query;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const sitesCount = computed(() => {
|
||||
return sources.value?.length ?? 0;
|
||||
});
|
||||
|
||||
const topDomains = computed(() => {
|
||||
return [...new Set((sources.value ?? []).map(s => s.hostname))].slice(0, 4);
|
||||
});
|
||||
|
||||
const failedFavicons = ref(new Set<string>());
|
||||
|
||||
const onFaviconError = (domain: string) => {
|
||||
failedFavicons.value.add(domain);
|
||||
};
|
||||
|
||||
const getDomainBadgeStyle = (idx: number) => {
|
||||
const zIndex = topDomains.value.length - idx;
|
||||
if (idx === 0) {
|
||||
return { zIndex };
|
||||
}
|
||||
return {
|
||||
zIndex,
|
||||
maskImage: "radial-gradient(circle 12.5px at -4px 50%, transparent 12.5px, #000 13px)",
|
||||
WebkitMaskImage: "radial-gradient(circle 12.5px at -4px 50%, transparent 12.5px, #000 13px)",
|
||||
};
|
||||
};
|
||||
|
||||
const title = computed(() => {
|
||||
if (props.toolCall.status === "pending") {
|
||||
return "Searching the web…";
|
||||
}
|
||||
if (props.toolCall.status === "failed") {
|
||||
return "Search failed";
|
||||
}
|
||||
if (props.toolCall.status === "cancelled") {
|
||||
return "Search cancelled";
|
||||
}
|
||||
const count = sitesCount.value;
|
||||
if (count > 0) {
|
||||
return `Searched ${count} ${count === 1 ? "site" : "sites"}`;
|
||||
}
|
||||
return "Searched the web";
|
||||
});
|
||||
|
||||
const formattedQuery = computed(() => {
|
||||
return query.value ? `"${query.value}"` : "";
|
||||
});
|
||||
|
||||
const customTab = computed(() => {
|
||||
const count = sources.value?.length ?? 0;
|
||||
return {
|
||||
id: "sources",
|
||||
label: "Sources",
|
||||
count: `${count} ${count === 1 ? "source" : "sources"}`,
|
||||
copyContent: () => {
|
||||
if (!sources.value?.length) {
|
||||
return "";
|
||||
}
|
||||
return sources.value.map(s => `${s.title}\n${s.url}`).join("\n\n");
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Disclosure
|
||||
:tool-call="toolCall"
|
||||
:part="part"
|
||||
:title="title"
|
||||
:subtitle="formattedQuery"
|
||||
icon="i-mynaui-search"
|
||||
:custom-tab="customTab"
|
||||
@retry="emit('retry')"
|
||||
>
|
||||
<template #summary>
|
||||
<Tooltip
|
||||
v-if="warnings.length"
|
||||
class="flex items-center text-amber-500 mr-2"
|
||||
:text="`Some search engines failed: ${warningSummary}`"
|
||||
>
|
||||
<span
|
||||
class="i-mynaui-danger-triangle w-4 h-4"
|
||||
role="img"
|
||||
:aria-label="`Some search engines failed: ${warningSummary}`"
|
||||
></span>
|
||||
</Tooltip>
|
||||
<div v-if="topDomains.length && toolCall.status !== 'pending'" class="flex items-center mr-1" aria-hidden="true">
|
||||
<span
|
||||
v-for="(domain, idx) in topDomains"
|
||||
:key="domain"
|
||||
class="relative w-[22px] h-[22px] rounded-full overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center shrink-0"
|
||||
:class="idx > 0 ? '-ml-[7px]' : ''"
|
||||
:style="getDomainBadgeStyle(idx)"
|
||||
:title="domain"
|
||||
>
|
||||
<img
|
||||
v-if="!failedFavicons.has(domain)"
|
||||
:src="getFaviconUrl(domain)"
|
||||
:alt="domain"
|
||||
class="w-[15px] h-[15px] rounded-full object-contain"
|
||||
loading="lazy"
|
||||
@error="onFaviconError(domain)"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center text-[10px] font-bold text-white"
|
||||
:style="{ backgroundColor: getDomainColor(domain) }"
|
||||
>
|
||||
{{ domainInitial(domain) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #sources>
|
||||
<div class="flex flex-col gap-2 max-h-80 overflow-y-auto scrollbar-thin p-3">
|
||||
<p v-if="warnings.length" class="flex items-start gap-2 text-xs text-amber-500 m-0">
|
||||
<span class="i-mynaui-danger-triangle w-4 h-4 shrink-0" aria-hidden="true"></span>
|
||||
<span>Some search engines failed: {{ warningSummary }}</span>
|
||||
</p>
|
||||
<a
|
||||
v-for="source in sources"
|
||||
:key="source.url"
|
||||
:href="source.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex flex-col gap-1.5 p-3 rounded-xl bg-[var(--bg-container)] @hover:bg-[var(--color-hover)] transition-colors cursor-pointer text-left no-underline border border-transparent @hover:border-[var(--color-border)]"
|
||||
>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex items-center gap-2.5 min-w-0">
|
||||
<span class="w-6 h-6 rounded-lg overflow-hidden flex items-center justify-center shrink-0 bg-[var(--bg-surface)]">
|
||||
<img
|
||||
v-if="!failedFavicons.has(source.hostname)"
|
||||
:src="getFaviconUrl(source.hostname)"
|
||||
:alt="source.hostname"
|
||||
class="w-4 h-4 rounded-md object-contain"
|
||||
loading="lazy"
|
||||
@error="onFaviconError(source.hostname)"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center text-xs font-bold text-white"
|
||||
:style="{ backgroundColor: getDomainColor(source.hostname) }"
|
||||
>
|
||||
{{ domainInitial(source.hostname) }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-sm font-medium text-[var(--text-primary)] truncate">
|
||||
{{ source.title }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-[11px] font-mono text-[var(--text-dim)] truncate pl-0.5">
|
||||
{{ source.url }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="source.snippet" class="text-xs text-[var(--text-secondary)] leading-relaxed m-0 line-clamp-2">
|
||||
{{ source.snippet }}
|
||||
</p>
|
||||
</a>
|
||||
<p v-if="!sources?.length" class="text-xs text-[var(--text-tertiary)] m-0">
|
||||
{{ toolCall.status === "pending" ? "Waiting for results…" : "No usable sources returned." }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</Disclosure>
|
||||
</template>
|
||||
@@ -1,44 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToolCall } from '~/composables/useChat';
|
||||
import Debug from './Debug.vue';
|
||||
import type { MessagePart, ToolCall } from "~/composables/useChat";
|
||||
import { computed } from "vue";
|
||||
import Generic from "./Generic.vue";
|
||||
import { resolveToolRenderer } from "./renderers";
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: Readonly<ToolCall>;
|
||||
part?: MessagePart;
|
||||
}>();
|
||||
|
||||
const deubgToolCallOpen = ref(false);
|
||||
const emit = defineEmits<{
|
||||
retry: [];
|
||||
}>();
|
||||
|
||||
const toggleDebugToolCall = () => {
|
||||
deubgToolCallOpen.value = !deubgToolCallOpen.value;
|
||||
};
|
||||
const renderer = computed(() => resolveToolRenderer(props.toolCall.toolName));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
class="select-none w-full @hover:bg-[var(--color-hover)] group rounded-lg p-1 flex justify-between items-center text-[--text-tertiary] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center">
|
||||
<span class="w-3 h-3" :class="{
|
||||
'i-svg-spinners-180-ring-with-bg': toolCall.status === 'pending',
|
||||
'i-mynaui-x-solid text-[#ff3b3b]': toolCall.status === 'failed',
|
||||
'i-mynaui-tool': toolCall.status !== 'pending' && toolCall.status !== 'failed',
|
||||
}"></span>
|
||||
</div>
|
||||
{{ toolCall.toolName }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<button @click="toggleDebugToolCall"
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden @hover:bg-[var(--color-hover)] flex items-center justify-center transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<span class="i-mynaui-search w-3 h-3 text-[var(--text-secondary)]"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Debug v-if="deubgToolCallOpen" :toolCall="toolCall" />
|
||||
<div class="flex flex-col min-w-0">
|
||||
<NuxtErrorBoundary :key="toolCall.id + ':' + toolCall.toolName">
|
||||
<component :is="renderer" :tool-call="toolCall" :part="part" @retry="emit('retry')"></component>
|
||||
<template #error>
|
||||
<Generic :tool-call="toolCall" :part="part" @retry="emit('retry')"></Generic>
|
||||
</template>
|
||||
</NuxtErrorBoundary>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Component } from "vue";
|
||||
import Generic from "./Generic.vue";
|
||||
import Search from "./Search.vue";
|
||||
|
||||
// Renderers receive a readonly toolCall prop and own their presentation only.
|
||||
const renderers: Readonly<Record<string, Component>> = Object.freeze({
|
||||
search: Search,
|
||||
});
|
||||
|
||||
export const resolveToolRenderer = (toolName: string): Component => {
|
||||
if (Object.hasOwn(renderers, toolName)) {
|
||||
return renderers[toolName] ?? Generic;
|
||||
}
|
||||
return Generic;
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { collectCitationSources } from '~/utils/citations';
|
||||
import type { SearchSource } from '~/utils/tool-rendering';
|
||||
import Error from './Error.vue';
|
||||
import Reasoning from './Reasoning.vue';
|
||||
import Text from './Text.vue';
|
||||
import Tool from './Tool/index.vue';
|
||||
import type { MessageEntity } from '~/composables/useChat';
|
||||
|
||||
@@ -9,6 +10,14 @@ const props = defineProps<{
|
||||
message: MessageEntity
|
||||
}>();
|
||||
|
||||
const EMPTY_SOURCES: ReadonlyMap<string, SearchSource> = new Map();
|
||||
|
||||
// Citation markers are rare; avoid JSON-parsing every tool output when none exist.
|
||||
const sources = computed(() => {
|
||||
const parts = props.message.parts ?? [];
|
||||
const hasCitations = parts.some(part => part.type === 'text' && part.content?.includes('[[cite:') === true);
|
||||
return hasCitations ? collectCitationSources(parts) : EMPTY_SOURCES;
|
||||
});
|
||||
const triggerRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const { open, close, isOpen } = useTokenDropdown();
|
||||
@@ -25,14 +34,17 @@ const toggleTokenDropdown = () => {
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div v-for="part in message.parts?.filter(p => p.content?.trim() !== '' || p.toolCall)" :key="part.id">
|
||||
<Reasoning v-if="part.type === 'reasoning'" :part="part" />
|
||||
<Text v-else-if="part.type === 'text'" :part="part" />
|
||||
<Tool v-else-if="part.type === 'tool-call'" :toolCall="part.toolCall!" />
|
||||
<template v-for="part in message.parts" :key="part.id">
|
||||
<template v-if="part.content?.trim() !== '' || part.toolCall">
|
||||
<Reasoning v-if="part.type === 'reasoning'" :part="part"></Reasoning>
|
||||
<MarkdownRenderer v-else-if="part.type === 'text'" :finished="part.finished" :id="part.id"
|
||||
:content="part.content!" :sources="sources" :citations="true"></MarkdownRenderer>
|
||||
<Tool v-else-if="part.type === 'tool-call'" :tool-call="part.toolCall!" :part="part"></Tool>
|
||||
<div v-else>
|
||||
Unhandled part type: {{ part.type }} {{ part }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<span class="flex items-center gap-1"
|
||||
v-if="message.generation?.status === 'pending' && (message.parts || []).length === 0">
|
||||
|
||||
@@ -7,6 +7,10 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-end gap-1 max-w-full">
|
||||
<time class="text-[10px] text-[var(--text-secondary)] opacity-60 select-none pr-1">
|
||||
{{ new Date(message.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}
|
||||
</time>
|
||||
<div class="flex flex-col gap-2 max-w-full bg-[var(--bg-container)] py-2 px-3 rounded-xl">
|
||||
<MarkdownRenderer v-if="message.content" :finished="true" :content="message.content" :id="message.id" />
|
||||
<div v-if="message.attachments && message.attachments.length > 0" class="flex flex-col gap-2">
|
||||
@@ -19,4 +23,5 @@ defineProps<{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Message } from '~/composables/useChat';
|
||||
import { getActiveMessage, getCurrentChildIndex, getMessageCount, getNextActiveChildId } from '~~/utils/message';
|
||||
|
||||
const { openDialog } = useDialog();
|
||||
|
||||
@@ -12,6 +13,7 @@ const emit = defineEmits<{
|
||||
delete: [];
|
||||
edit: [value: string];
|
||||
patch: [updates: Partial<Message>];
|
||||
fork: [];
|
||||
}>();
|
||||
|
||||
let reqAbortController: AbortController | null = null;
|
||||
@@ -34,7 +36,9 @@ watch(() => message.activeChildId, async (newValue) => {
|
||||
});
|
||||
|
||||
watch(() => message.children.length, (newCount, oldCount) => {
|
||||
if (newCount === oldCount) return;
|
||||
if (newCount === oldCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newCount === 0) {
|
||||
emit('patch', { activeChildId: null });
|
||||
@@ -42,7 +46,7 @@ watch(() => message.children.length, (newCount, oldCount) => {
|
||||
}
|
||||
|
||||
if (oldCount > newCount) {
|
||||
const activeChild = message.children.find(c => c.id === message.activeChildId);
|
||||
const activeChild = message.children.find(c => c?.id === message.activeChildId);
|
||||
if (!activeChild) {
|
||||
emit('patch', { activeChildId: message.children[newCount - 1]?.id ?? null });
|
||||
}
|
||||
@@ -55,19 +59,17 @@ watch(() => message.children.length, (newCount, oldCount) => {
|
||||
}
|
||||
});
|
||||
|
||||
const activeMessage = computed(() => {
|
||||
if (message.activeChildId && message.children.length > 0) {
|
||||
const child = message.children.find(c => c.id === message.activeChildId);
|
||||
if (child) return child;
|
||||
watch(() => message.deleted, (isDeleted) => {
|
||||
if (isDeleted && message.children.length > 0) {
|
||||
const activeChild = message.children.find(c => c?.id === message.activeChildId);
|
||||
if (!activeChild) {
|
||||
emit('patch', { activeChildId: message.children[0]?.id ?? null });
|
||||
}
|
||||
|
||||
if (message.deleted === true && message.children.length > 0) {
|
||||
return message.children[0];
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
|
||||
const activeMessage = computed(() => getActiveMessage(message));
|
||||
|
||||
const copied = ref(false);
|
||||
const copyMessage = async () => {
|
||||
if (activeMessage.value!.role === 'user') {
|
||||
@@ -126,91 +128,126 @@ const handleEdit = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const forkMessage = () => {
|
||||
emit('fork');
|
||||
};
|
||||
|
||||
const deleteMessage = () => {
|
||||
emit('delete');
|
||||
};
|
||||
|
||||
const messageCount = computed(() => {
|
||||
if (message.deleted === true) {
|
||||
return message.children.length;
|
||||
}
|
||||
const messageCount = computed(() => getMessageCount(message));
|
||||
|
||||
return message.children.length + 1;
|
||||
});
|
||||
|
||||
const currentChildIndex = computed(() => {
|
||||
if (!message.activeChildId) return 0;
|
||||
const idx = message.children.findIndex(c => c.id === message.activeChildId);
|
||||
return idx >= 0 ? idx + 1 : 0;
|
||||
});
|
||||
const currentChildIndex = computed(() => getCurrentChildIndex(message));
|
||||
|
||||
const navigateChild = (direction: -1 | 1) => {
|
||||
const allItems = message.deleted === true
|
||||
? message.children
|
||||
: [null, ...message.children];
|
||||
const nextActiveChildId = getNextActiveChildId(message, direction);
|
||||
emit('patch', { activeChildId: nextActiveChildId });
|
||||
};
|
||||
|
||||
const currentIdx = allItems.findIndex(item =>
|
||||
item === null ? !message.activeChildId : item.id === message.activeChildId
|
||||
);
|
||||
const { show: showTooltip, hide: hideTooltip } = useTooltip();
|
||||
|
||||
const newIdx = Math.max(0, Math.min(allItems.length - 1, currentIdx + direction));
|
||||
const newItem = allItems[newIdx];
|
||||
let hasCursor: boolean | null = null;
|
||||
const checkHasCursor = () => {
|
||||
if (hasCursor === null && typeof window !== 'undefined') {
|
||||
hasCursor = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||
}
|
||||
return hasCursor ?? false;
|
||||
};
|
||||
|
||||
emit('patch', { activeChildId: newItem?.id ?? null });
|
||||
const handleTooltipHover = (e: MouseEvent) => {
|
||||
if (!checkHasCursor()) {
|
||||
return;
|
||||
}
|
||||
const target = (e.target as HTMLElement).closest('[data-hotkey], [data-tooltip]') as HTMLElement | null;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const hotkey = target.dataset.hotkey ? target.dataset.hotkey.split(',') : undefined;
|
||||
const text = target.dataset.tooltip || undefined;
|
||||
showTooltip(target, hotkey, text);
|
||||
};
|
||||
|
||||
const handleTooltipLeave = () => {
|
||||
hideTooltip();
|
||||
};
|
||||
|
||||
const handleAction = (e: MouseEvent) => {
|
||||
const trigger = (e.target as HTMLElement).closest('[data-action]') as HTMLElement | null;
|
||||
if (!trigger) {
|
||||
return;
|
||||
}
|
||||
switch (trigger.dataset.action) {
|
||||
case 'prev-child':
|
||||
navigateChild(-1);
|
||||
break;
|
||||
case 'next-child':
|
||||
navigateChild(1);
|
||||
break;
|
||||
case 'regenerate':
|
||||
regenerateMessage();
|
||||
break;
|
||||
case 'edit':
|
||||
handleEdit();
|
||||
break;
|
||||
case 'copy':
|
||||
copyMessage();
|
||||
break;
|
||||
case 'fork':
|
||||
forkMessage();
|
||||
break;
|
||||
case 'delete':
|
||||
deleteMessage();
|
||||
break;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="activeMessage" class="max-w-full mb-4 group flex flex-col">
|
||||
<div :class="[message.role === 'user' ? 'pl-9 flex justify-end' : '']">
|
||||
<MessageUser v-if="message.role === 'user'" :message="activeMessage!" />
|
||||
<MessageAgent v-else :message="activeMessage" />
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<div v-if="messageCount > 1" class="flex gap-1">
|
||||
<Tooltip :inert="currentChildIndex === 0" :hotkey="['alt', '[']">
|
||||
<button @click="navigateChild(-1)"
|
||||
<MessageUser v-if="message.role === 'user'" class="pl-9 self-end" :message="activeMessage!"></MessageUser>
|
||||
<MessageAgent v-else :message="activeMessage"></MessageAgent>
|
||||
<div class="flex justify-between items-center" @click="handleAction">
|
||||
<div v-if="messageCount > 1" class="flex gap-1"
|
||||
@mouseover="handleTooltipHover" @mouseout="handleTooltipLeave">
|
||||
<button data-action="prev-child" :data-hotkey="currentChildIndex > 0 ? 'alt,[' : undefined"
|
||||
class="@hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<span class="i-mynaui-chevron-left w-4 h-4 text-[var(--text-secondary)]"
|
||||
<span class="i-mynaui-chevron-left w-4 h-4 text-[var(--text-secondary)] pointer-events-none"
|
||||
:class="currentChildIndex === 0 ? 'opacity-0' : ''"></span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span class="text-xs text-[var(--text-secondary)]">
|
||||
{{ currentChildIndex + 1 }} / {{ messageCount }}
|
||||
</span>
|
||||
<Tooltip :inert="currentChildIndex + 1 === messageCount" :hotkey="['alt', ']']">
|
||||
<button @click="navigateChild(1)"
|
||||
<button data-action="next-child" :data-hotkey="currentChildIndex + 1 < messageCount ? 'alt,]' : undefined"
|
||||
class="@hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<span class="i-mynaui-chevron-right w-4 h-4 text-[var(--text-secondary)]"
|
||||
<span class="i-mynaui-chevron-right w-4 h-4 text-[var(--text-secondary)] pointer-events-none"
|
||||
:class="currentChildIndex + 1 === messageCount ? 'opacity-0' : ''"></span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="activeMessage.generation?.status !== 'pending'"
|
||||
class="self-end mt-1 w-fit bg-[var(--bg-container)] text-[var(--text-secondary)] gap-px flex items-center rounded-md overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Tooltip :hotkey="['ctrl', 'shift', 'enter']">
|
||||
<button @click="regenerateMessage"
|
||||
@mouseover="handleTooltipHover" @mouseout="handleTooltipLeave"
|
||||
class="ml-auto self-end mt-1 w-fit bg-[var(--bg-container)] text-[var(--text-secondary)] gap-px flex items-center rounded-md overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<button data-action="regenerate" data-hotkey="ctrl,shift,enter"
|
||||
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
|
||||
<span class="i-mynaui-refresh text-4.5"></span>
|
||||
<span class="i-mynaui-refresh text-4.5 pointer-events-none"></span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button v-if="message.role === 'user'" @click="handleEdit"
|
||||
<button v-if="message.role === 'user'" data-action="edit"
|
||||
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
|
||||
<span class="i-mynaui-pencil text-4.5"></span>
|
||||
<span class="i-mynaui-pencil text-4.5 pointer-events-none"></span>
|
||||
</button>
|
||||
<button @click="copyMessage" :class="{ 'text-emerald-500': copied }"
|
||||
<button data-action="copy" :class="{ 'text-emerald-500': copied }"
|
||||
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
|
||||
<span :class="copied ? 'i-mynaui-check text-emerald-500' : 'i-mynaui-copy text-4.5'"></span>
|
||||
<span :class="copied ? 'i-mynaui-check text-emerald-500' : 'i-mynaui-copy text-4.5'" class="pointer-events-none"></span>
|
||||
</button>
|
||||
<Tooltip :hotkey="['ctrl', 'shift', 'backspace']">
|
||||
<button @click="deleteMessage"
|
||||
<button data-action="fork"
|
||||
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
|
||||
<span class="i-mynaui-git-branch text-4.5 pointer-events-none"></span>
|
||||
</button>
|
||||
<button data-action="delete" data-hotkey="ctrl,shift,backspace"
|
||||
class="flex justify-center items-center w-7 h-6 text-red-500 @hover:bg-[var(--color-hover)]">
|
||||
<span class="i-mynaui-trash text-5"></span>
|
||||
<span class="i-mynaui-trash text-5 pointer-events-none"></span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogType } from '~/composables/useDialog';
|
||||
|
||||
const { openDialog } = await useDialog();
|
||||
|
||||
const props = defineProps<{
|
||||
enabled: boolean;
|
||||
maxResults: number;
|
||||
rerank: boolean;
|
||||
}>();
|
||||
defineProps<{ enabled: boolean }>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:enabled': [value: boolean];
|
||||
'update:maxResults': [value: number];
|
||||
'update:rerank': [value: boolean];
|
||||
}>();
|
||||
|
||||
const adjustMaxResults = (delta: number) => {
|
||||
const next = Math.min(50, Math.max(1, props.maxResults + delta));
|
||||
emit('update:maxResults', next);
|
||||
};
|
||||
|
||||
const { settings } = await useUserSettings();
|
||||
|
||||
const systemAssistantsRerank = computed(() => {
|
||||
return settings.value.systemAssistants?.rerank ?? null;
|
||||
});
|
||||
|
||||
const isRerankConfigured = computed(() => {
|
||||
const sa = systemAssistantsRerank.value;
|
||||
return sa?.enabled === true && sa?.modelId != null && sa.modelId !== '';
|
||||
});
|
||||
|
||||
const openSettings = () => {
|
||||
openDialog(DialogType.Settings, undefined, { page: 'systemAssistants' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -46,10 +16,8 @@ const openSettings = () => {
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #dropdown="{ close }">
|
||||
<div class="flex p-1 gap-4">
|
||||
<!-- Left: mode toggle group -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<template #dropdown>
|
||||
<div class="flex flex-col gap-1 p-1">
|
||||
<button @click="emit('update:enabled', false)"
|
||||
class="flex items-start gap-3 rounded-xl px-3 py-2.5 text-left transition-colors duration-150"
|
||||
:class="!enabled
|
||||
@@ -83,58 +51,11 @@ const openSettings = () => {
|
||||
</span>
|
||||
<span class="text-xs leading-snug"
|
||||
:class="enabled ? 'text-[var(--text-secondary)]' : 'text-[var(--text-dim)]'">
|
||||
Search the web automatically when needed
|
||||
Search the web and fetch URLs when needed
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Right: settings (only when enabled) -->
|
||||
<div v-if="enabled" class="flex flex-col gap-3 border-l border-[var(--color-border)] pl-4">
|
||||
<!-- Max results stepper -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Max results
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="adjustMaxResults(-1)"
|
||||
class="h-7 w-7 flex items-center justify-center rounded-lg @hover:bg-[var(--color-hover)] text-[var(--text-secondary)] @hover:text-[var(--text-primary)] transition-colors duration-150">
|
||||
<span class="text-sm i-mynaui-minus"></span>
|
||||
</button>
|
||||
<span class="w-8 text-center text-sm font-medium tabular-nums text-[var(--text-primary)]">
|
||||
{{ maxResults }}
|
||||
</span>
|
||||
<button @click="adjustMaxResults(1)"
|
||||
class="h-7 w-7 flex items-center justify-center rounded-lg @hover:bg-[var(--color-hover)] text-[var(--text-secondary)] @hover:text-[var(--text-primary)] transition-colors duration-150">
|
||||
<span class="text-sm i-mynaui-plus-solid"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rerank toggle -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium text-[var(--color-tertiary)] uppercase tracking-wider">
|
||||
Rerank
|
||||
</span>
|
||||
<div v-if="!isRerankConfigured"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[var(--bg-container)] border border-[var(--color-border)]">
|
||||
<button @click="() => { openSettings(); close() }"
|
||||
class="flex items-center gap-2 text-xs text-[var(--text-dim)] hover:text-[var(--text-primary)] transition-colors duration-150">
|
||||
<span class="i-mynaui-sparkles text-3.5"></span>
|
||||
<span>Setup reranking</span>
|
||||
<span class="i-mynaui-arrow-right text-3 ml-auto"></span>
|
||||
</button>
|
||||
</div>
|
||||
<label v-else
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[var(--bg-container)] border border-[var(--color-border)] cursor-pointer @hover:border-[var(--color-hover)] transition-colors duration-150">
|
||||
<input type="checkbox" :checked="props.rerank"
|
||||
@change="emit('update:rerank', ($event.target as HTMLInputElement).checked)"
|
||||
class="w-4 h-4 rounded border-[var(--color-border)] bg-transparent text-[var(--color-accent)] focus:ring-[var(--color-accent)] focus:ring-offset-0 cursor-pointer" />
|
||||
<span class="text-xs text-[var(--text-secondary)]">Rerank results</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
|
||||
const props = defineProps<{
|
||||
params?: string;
|
||||
}>();
|
||||
|
||||
const { providers, allModels } = await useModels();
|
||||
const { settings, updateSettings } = await useUserSettings();
|
||||
|
||||
const toggle = async (key: string) => {
|
||||
const current = (settings.value.systemAssistants as any)[key];
|
||||
const toggle = async () => {
|
||||
const current = settings.value.systemAssistants.rename;
|
||||
await updateSettings({
|
||||
systemAssistants: {
|
||||
[key]: {
|
||||
rename: {
|
||||
...current,
|
||||
enabled: !current.enabled
|
||||
}
|
||||
@@ -20,11 +16,11 @@ const toggle = async (key: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const updateModel = async (key: string, model: ModelWithProvider | undefined | null) => {
|
||||
const current = (settings.value.systemAssistants as any)[key];
|
||||
const updateModel = async (model: ModelWithProvider | undefined | null) => {
|
||||
const current = settings.value.systemAssistants.rename;
|
||||
await updateSettings({
|
||||
systemAssistants: {
|
||||
[key]: {
|
||||
rename: {
|
||||
...current,
|
||||
modelId: model?.id ?? null
|
||||
}
|
||||
@@ -44,15 +40,15 @@ defineEmits(['navigate']);
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-4 py-4 flex-grow overflow-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]">
|
||||
<div class="flex flex-col" v-for="(systemAssistant, key) in settings.systemAssistants" :key="key">
|
||||
<label :for="`slider-${key}`" class="flex justify-between gap-4 items-center">
|
||||
<h4 class="case-capital">{{ key }}</h4>
|
||||
<Slider :id="`slider-${key}`" :checked="systemAssistant?.enabled ?? false"
|
||||
@click="toggle(String(key))" />
|
||||
<div class="flex flex-col">
|
||||
<label for="slider-rename" class="flex justify-between gap-4 items-center">
|
||||
<h4 class="case-capital">Rename</h4>
|
||||
<Slider id="slider-rename" :checked="settings.systemAssistants.rename?.enabled ?? false"
|
||||
@click="toggle" />
|
||||
</label>
|
||||
<div class="flex-1 justify-between gap-4 items-center">
|
||||
<ModelSelector :providers="providers" :model-value="getModel(systemAssistant?.modelId)"
|
||||
@update:model-value="(model) => updateModel(String(key), model)" />
|
||||
<ModelSelector :providers="providers" :model-value="getModel(settings.systemAssistants.rename?.modelId)"
|
||||
@update:model-value="updateModel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import RowVirtualizerFixed from '~/components/RowVirtualizerFixed.vue';
|
||||
import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue';
|
||||
import { DialogType } from '~/composables/useDialog';
|
||||
import { exportTopicToJson, exportTopicToMarkdown } from '~~/utils/export';
|
||||
|
||||
const { openDialog } = useDialog();
|
||||
const dropdownOpen = ref(false);
|
||||
const dropdownTrigger = ref<HTMLElement | null>(null);
|
||||
const dropdownContent = ref(null);
|
||||
const activeMenuTopicId = ref<string | null>(null);
|
||||
const exportSubmenuOpen = ref(false);
|
||||
const exportTrigger = ref<HTMLElement | null>(null);
|
||||
const exportContent = ref(null);
|
||||
const exportInProgress = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
const { getAgent, patchTopicLocally, deleteTopic: deleteAgentTopic } = await useAgents();
|
||||
@@ -32,16 +39,51 @@ const { floatingStyles, placement, middlewareData } = useFloating(dropdownTrigge
|
||||
transform: false,
|
||||
});
|
||||
|
||||
const {
|
||||
floatingStyles: exportFloatingStyles,
|
||||
placement: exportPlacement,
|
||||
middlewareData: exportMiddlewareData,
|
||||
} = useFloating(exportTrigger, exportContent, {
|
||||
placement: 'right-start',
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [offset(6), flip({ fallbackPlacements: ['left-start', 'right-end', 'left-end'] }), shift({ padding: 10 }), hide()],
|
||||
transform: false,
|
||||
});
|
||||
|
||||
const transformOrigin = computed(() =>
|
||||
placement.value.startsWith('top')
|
||||
? 'transform-origin-bottom-center'
|
||||
: 'transform-origin-top-center'
|
||||
);
|
||||
|
||||
const exportTransformOrigin = computed(() => {
|
||||
switch (exportPlacement.value.split('-')[0]) {
|
||||
case 'left':
|
||||
return 'transform-origin-right-center';
|
||||
case 'right':
|
||||
return 'transform-origin-left-center';
|
||||
case 'top':
|
||||
return 'transform-origin-bottom-center';
|
||||
case 'bottom':
|
||||
return 'transform-origin-top-center';
|
||||
default:
|
||||
return 'transform-origin-left-center';
|
||||
}
|
||||
});
|
||||
|
||||
const closeExportSubmenu = () => {
|
||||
exportSubmenuOpen.value = false;
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
dropdownOpen.value = false;
|
||||
activeMenuTopicId.value = null;
|
||||
dropdownTrigger.value = null;
|
||||
closeExportSubmenu();
|
||||
};
|
||||
|
||||
const openExportSubmenu = () => {
|
||||
exportSubmenuOpen.value = true;
|
||||
};
|
||||
|
||||
// Computed to get the topic data for the currently open menu
|
||||
@@ -49,6 +91,35 @@ const menuTopic = computed(() =>
|
||||
topics.value.find(t => t.id === activeMenuTopicId.value)
|
||||
);
|
||||
|
||||
const handleExport = async (format: 'json' | 'markdown') => {
|
||||
if (!menuTopic.value || exportInProgress.value) return;
|
||||
|
||||
exportInProgress.value = true;
|
||||
try {
|
||||
if (format === 'json') {
|
||||
await exportTopicToJson(menuTopic.value.id);
|
||||
} else {
|
||||
await exportTopicToMarkdown(menuTopic.value.id);
|
||||
}
|
||||
} finally {
|
||||
exportInProgress.value = false;
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDropdownClickOutside = (event?: Event) => {
|
||||
const target = event?.target as Node | null;
|
||||
if (target) {
|
||||
if (exportContent.value && (exportContent.value as HTMLElement).contains(target)) {
|
||||
return;
|
||||
}
|
||||
if (exportTrigger.value && exportTrigger.value.contains(target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const topicsOpen = ref(true);
|
||||
|
||||
const autoRenameTopic = async (topicId: string) => {
|
||||
@@ -56,10 +127,12 @@ const autoRenameTopic = async (topicId: string) => {
|
||||
}
|
||||
|
||||
const cancelAutoRename = async (topicId: string) => {
|
||||
// TODO: make this more optimistic
|
||||
await $fetch(`/api/topic/${topicId}/auto-rename/cancel`, {
|
||||
method: 'POST',
|
||||
const result = await $fetch(`/api/topic/${topicId}/auto-rename/cancel`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (result.ok) {
|
||||
patchTopicLocally(topicId, { renaming: false });
|
||||
}
|
||||
}
|
||||
|
||||
const renameTopicId = ref<string | null>(null);
|
||||
@@ -133,6 +206,7 @@ const handleNavClick = (e: MouseEvent) => {
|
||||
activeMenuTopicId.value = topicId;
|
||||
dropdownTrigger.value = trigger;
|
||||
dropdownOpen.value = true;
|
||||
closeExportSubmenu();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -151,6 +225,17 @@ onMounted(() => {
|
||||
<SidenavItem :to="`/agent/${agentId}/profile`" name="Agent Info" icon="i-mynaui-info-square"
|
||||
:active="route.path.endsWith('/profile')" />
|
||||
|
||||
<SidenavItem @click="openDialog(DialogType.QuickSwitcher)" name="Search" icon="i-mynaui-search">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="flex bg-[var(--bg-container)] px-1 rounded border border-[var(--color-border)]">
|
||||
<kbd class="font-mono text-[10px] case-capital">ctrl</kbd>
|
||||
</span>
|
||||
<span class="flex bg-[var(--bg-container)] px-1 rounded border border-[var(--color-border)]">
|
||||
<kbd class="font-mono text-[10px] case-capital">k</kbd>
|
||||
</span>
|
||||
</div>
|
||||
</SidenavItem>
|
||||
|
||||
<!-- Topics Section -->
|
||||
<button @click="topicsOpen = !topicsOpen"
|
||||
class="flex items-center justify-between gap-2 px-2 py-2 rounded-lg bg-transparent @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] transition-colors w-full text-left">
|
||||
@@ -173,12 +258,13 @@ onMounted(() => {
|
||||
v-model="newTopicName" @keydown.enter="saveRename" @keydown.escape="cancelRename"
|
||||
@blur="saveRename"
|
||||
class="flex-1 bg-transparent border-none outline-none md:text-sm font-medium text-[var(--text-primary)] px-0 min-w-0" />
|
||||
<div v-else-if="topic.renaming" class="flex w-full">
|
||||
<span class="i-svg-spinners-3-dots-fade text-7 md:text-6"></span>
|
||||
</div>
|
||||
<span v-else class="md:text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<div v-else class="flex items-center gap-2 min-w-0">
|
||||
<span class="md:text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{{ topic.name }}
|
||||
</span>
|
||||
<span v-if="topic.renaming" aria-label="Renaming"
|
||||
class="i-svg-spinners-3-dots-fade shrink-0 text-4"></span>
|
||||
</div>
|
||||
|
||||
<div data-action="toggle-dropdown"
|
||||
class="text-[var(--text-secondary)] shrink-0 w-0 max-w-0 opacity-0 overflow-hidden p-1 max-md:w-auto max-md:max-w-none max-md:opacity-100 flex items-center justify-center rounded-md @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)]"
|
||||
@@ -198,36 +284,49 @@ onMounted(() => {
|
||||
leave-active-class="transition-[opacity,transform] duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
leave-from-class="opacity-100 scale-100 translate-y-0"
|
||||
leave-to-class="opacity-0 scale-95 translate-y-1">
|
||||
<div v-if="dropdownOpen" ref="dropdownContent" :style="{
|
||||
<div v-if="dropdownOpen" ref="dropdownContent" v-click-outside="handleDropdownClickOutside" :style="{
|
||||
...floatingStyles,
|
||||
visibility: middlewareData.hide?.referenceHidden
|
||||
? 'hidden'
|
||||
: 'visible',
|
||||
}" class="fixed z-20" :class="transformOrigin">
|
||||
<div v-click-outside="closeDropdown"
|
||||
<div
|
||||
class="bg-[var(--bg-surface)] border border-[var(--color-border)] rounded-xl p-1.5 shadow-xl flex flex-col gap-1 min-w-40">
|
||||
|
||||
|
||||
<!-- Dynamic content based on menuTopic -->
|
||||
<template v-if="menuTopic">
|
||||
<button v-if="menuTopic.renaming" @click="cancelAutoRename(menuTopic.id); closeDropdown()"
|
||||
@mouseenter="closeExportSubmenu"
|
||||
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50">
|
||||
Cancel Auto Rename
|
||||
</button>
|
||||
<button v-else @click="autoRenameTopic(menuTopic.id); closeDropdown()"
|
||||
@mouseenter="closeExportSubmenu"
|
||||
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50">
|
||||
Auto Rename
|
||||
</button>
|
||||
|
||||
<button :disabled="menuTopic.renaming ?? false"
|
||||
@click="startRename(menuTopic.id, menuTopic.name); closeDropdown()"
|
||||
@mouseenter="closeExportSubmenu"
|
||||
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50">
|
||||
Rename
|
||||
</button>
|
||||
|
||||
<button ref="exportTrigger" @click.stop="exportSubmenuOpen = !exportSubmenuOpen"
|
||||
@mouseenter="openExportSubmenu"
|
||||
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 flex items-center justify-between gap-3"
|
||||
:class="{ 'bg-[var(--color-hover)]': exportSubmenuOpen }"
|
||||
:disabled="exportInProgress">
|
||||
<span>Export</span>
|
||||
<span class="i-mynaui-chevron-right text-4 shrink-0"
|
||||
:class="exportPlacement.startsWith('left') ? 'rotate-180' : ''"></span>
|
||||
</button>
|
||||
|
||||
<div class="h-px bg-[var(--color-border)] my-1" />
|
||||
|
||||
<button @click="deleteTopic(menuTopic.id); closeDropdown()"
|
||||
@mouseenter="closeExportSubmenu"
|
||||
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 text-red-500">
|
||||
Delete
|
||||
</button>
|
||||
@@ -235,6 +334,32 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
enter-from-class="opacity-0 scale-95" enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition-[opacity,transform] duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
||||
<div v-if="dropdownOpen && exportSubmenuOpen" ref="exportContent" :style="{
|
||||
...exportFloatingStyles,
|
||||
visibility: exportMiddlewareData.hide?.referenceHidden
|
||||
? 'hidden'
|
||||
: 'visible',
|
||||
}" class="fixed z-30" :class="exportTransformOrigin"
|
||||
@mouseenter="openExportSubmenu">
|
||||
<div
|
||||
class="bg-[var(--bg-surface)] border border-[var(--color-border)] rounded-xl p-1.5 shadow-xl flex flex-col gap-1 min-w-44">
|
||||
<button :disabled="exportInProgress" @click="handleExport('json')"
|
||||
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 whitespace-nowrap">
|
||||
Export to JSON
|
||||
</button>
|
||||
<button :disabled="exportInProgress" @click="handleExport('markdown')"
|
||||
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 whitespace-nowrap">
|
||||
Export to Markdown
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
python: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:python': [value: boolean];
|
||||
}>();
|
||||
|
||||
const anyEnabled = computed(() => props.python);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dropdown dropdownClass="text-sm" placement="top">
|
||||
<template #default="{ toggle, setRef }">
|
||||
<button :ref="setRef" @click="toggle"
|
||||
class="flex items-center justify-center h-8.5 w-8.5 @hover:bg-[var(--color-hover)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<span class="pointer-events-none i-mynaui-archive text-5 transition-colors duration-200"
|
||||
:class="anyEnabled ? 'text-[var(--color-accent)]' : 'text-[var(--text-secondary)]'"></span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #dropdown>
|
||||
<div class="flex flex-col gap-1 p-1 min-w-48">
|
||||
<label
|
||||
class="flex items-start gap-3 rounded-xl px-3 py-2.5 text-left cursor-pointer transition-colors duration-150 @hover:bg-[var(--color-hover)]"
|
||||
:class="python ? 'bg-[var(--color-active)]' : ''">
|
||||
<input type="checkbox" :checked="python"
|
||||
@change="emit('update:python', ($event.target as HTMLInputElement).checked)"
|
||||
class="mt-0.5 w-4 h-4 rounded border-[var(--color-border)] bg-transparent text-[var(--color-accent)] focus:ring-[var(--color-accent)] focus:ring-offset-0 cursor-pointer shrink-0" />
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-sm font-medium"
|
||||
:class="python ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)]'">
|
||||
Python
|
||||
</span>
|
||||
<span class="text-xs leading-snug"
|
||||
:class="python ? 'text-[var(--text-secondary)]' : 'text-[var(--text-dim)]'">
|
||||
Run code in a sandbox
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
@@ -1,20 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
hotkey?: string[];
|
||||
text?: string;
|
||||
}>();
|
||||
|
||||
const { show, hide } = useTooltip();
|
||||
const triggerRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const hasCursor = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
hasCursor.value = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||
});
|
||||
let hasCursor: boolean | null = null;
|
||||
const checkHasCursor = () => {
|
||||
if (hasCursor === null && typeof window !== 'undefined') {
|
||||
hasCursor = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||
}
|
||||
return hasCursor ?? false;
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (triggerRef.value && hasCursor.value) {
|
||||
show(triggerRef.value, props.hotkey);
|
||||
if (triggerRef.value && checkHasCursor()) {
|
||||
show(triggerRef.value, props.hotkey, props.text);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue';
|
||||
|
||||
const { activeElement, activeHotkey, isVisible } = useTooltip();
|
||||
const { activeElement, activeHotkey, activeText, isVisible } = useTooltip();
|
||||
|
||||
const tooltipRef = ref<HTMLElement | null>(null);
|
||||
|
||||
@@ -61,7 +61,8 @@ const transformOrigin = computed(() => placement.value.includes('top') ? 'transf
|
||||
}
|
||||
]" :class="transformOrigin"
|
||||
class="pointer-events-none fixed z-35 flex flex-col bg-[var(--bg-surface)] rounded-lg shadow-xl px-1.5 py-1 text-xs">
|
||||
<div class="flex gap-1 items-center">
|
||||
<span v-if="activeText" class="max-w-xs whitespace-normal">{{ activeText }}</span>
|
||||
<div v-if="displayHotkey?.length" class="flex gap-1 items-center">
|
||||
<span v-for="key in displayHotkey" :key="key"
|
||||
class="bg-[var(--bg-container)] px-1 rounded border border-[var(--color-border)]">
|
||||
<kbd class="font-mono text-[10px] case-capital">{{ key }}</kbd>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { BaseMessage } from "~/composables/useChat";
|
||||
import { nanoid } from "nanoid";
|
||||
import { provisionalTopicTitle } from "~~/utils/topic-title";
|
||||
import { attempt } from "~~/types/result";
|
||||
import * as schema from '~~/drizzle/schema';
|
||||
|
||||
@@ -153,11 +155,11 @@ export const useAgents = async () => {
|
||||
|
||||
// TODO: since topics are contained within each agent struct, all topic
|
||||
// actions must be done through the agents composable
|
||||
const createTopic = async (agentId: string) => {
|
||||
const createTopic = async (agentId: string, message?: BaseMessage) => {
|
||||
const topicId = nanoid();
|
||||
const topic = {
|
||||
id: topicId,
|
||||
name: 'New Topic',
|
||||
name: provisionalTopicTitle(message),
|
||||
agentId,
|
||||
};
|
||||
|
||||
@@ -198,11 +200,46 @@ export const useAgents = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
const forkTopic = async (topicId: string, messageId: string): Promise<string | null> => {
|
||||
try {
|
||||
const result = await $fetch<{ ok: boolean; topicId: string; name: string }>(`/api/topic/${topicId}/fork`, {
|
||||
method: 'POST',
|
||||
body: { messageId },
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const agent = agents.value.find(a => a.topics.some(t => t.id === topicId));
|
||||
if (agent) {
|
||||
const newTopic: Topic = {
|
||||
id: result.topicId,
|
||||
name: result.name,
|
||||
agentId: agent.id,
|
||||
userId: agent.userId,
|
||||
renaming: false,
|
||||
renameAttemptId: null,
|
||||
renameExpiresAt: null,
|
||||
titleSource: "manual",
|
||||
createdAt: new Date(),
|
||||
};
|
||||
agents.value = agents.value.map(a =>
|
||||
a.id === agent.id ? { ...a, topics: [newTopic, ...a.topics] } as AgentWithTopics : a
|
||||
);
|
||||
}
|
||||
return result.topicId;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Failed to fork topic:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
agents,
|
||||
refresh,
|
||||
createAgent,
|
||||
createTopic,
|
||||
forkTopic,
|
||||
getAgent,
|
||||
patchAgentLocally,
|
||||
patchTopicLocally,
|
||||
|
||||
@@ -3,80 +3,295 @@ import { ref, watch, onUnmounted, type Ref } from 'vue';
|
||||
export function useAutoScroll(elementRef: Ref<HTMLElement | null>, options: {
|
||||
threshold?: number;
|
||||
} = {}) {
|
||||
const { threshold = 80 } = options;
|
||||
const { threshold = 30 } = options;
|
||||
|
||||
const isUserScrollingUp = ref(false);
|
||||
/** User intentionally left the bottom — stop following new content */
|
||||
const unhooked = ref(false);
|
||||
/** Back-compat alias for UI that keys off "user scrolled away" */
|
||||
const isUserScrollingUp = unhooked;
|
||||
const shouldAutoScroll = ref(true);
|
||||
|
||||
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
|
||||
let isProgrammatic = false;
|
||||
let lastScrollTop = 0;
|
||||
let mutationRaf: number | null = null;
|
||||
let anchorRaf: number | null = null;
|
||||
let anchorDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let observer: MutationObserver | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let lastWidth = 0;
|
||||
let anchor: { element: Element; offsetFromTop: number } | null = null;
|
||||
|
||||
const isAtBottom = (el: HTMLElement) =>
|
||||
el.scrollHeight - el.scrollTop - el.clientHeight <= threshold;
|
||||
|
||||
const setUnhooked = (value: boolean) => {
|
||||
unhooked.value = value;
|
||||
shouldAutoScroll.value = !value;
|
||||
if (!value) anchor = null;
|
||||
};
|
||||
|
||||
const scrollToBottom = (behavior: ScrollBehavior = 'instant') => {
|
||||
const el = elementRef.value;
|
||||
if (!el) return;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bottom = Math.max(0, el.scrollHeight - el.clientHeight);
|
||||
if (Math.abs(el.scrollTop - bottom) < 0.5) {
|
||||
setUnhooked(false);
|
||||
lastScrollTop = el.scrollTop;
|
||||
return;
|
||||
}
|
||||
|
||||
isProgrammatic = true;
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight,
|
||||
top: bottom,
|
||||
behavior,
|
||||
});
|
||||
shouldAutoScroll.value = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
isProgrammatic = false;
|
||||
if (elementRef.value && isAtBottom(elementRef.value)) {
|
||||
setUnhooked(false);
|
||||
lastScrollTop = elementRef.value.scrollTop;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const captureAnchor = (el: HTMLElement) => {
|
||||
if (!unhooked.value) {
|
||||
anchor = null;
|
||||
return;
|
||||
}
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const offsets = [4, 24, 48, 80, 120];
|
||||
|
||||
for (const y of offsets) {
|
||||
const target = document.elementFromPoint(x, rect.top + y);
|
||||
if (!target || target === el || !el.contains(target)) {
|
||||
continue;
|
||||
}
|
||||
anchor = {
|
||||
element: target,
|
||||
offsetFromTop: target.getBoundingClientRect().top - rect.top,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
anchor = null;
|
||||
};
|
||||
|
||||
const requestAnchorCapture = () => {
|
||||
if (!unhooked.value || anchorRaf !== null) return;
|
||||
anchorRaf = requestAnimationFrame(() => {
|
||||
anchorRaf = null;
|
||||
const el = elementRef.value;
|
||||
if (el) captureAnchor(el);
|
||||
});
|
||||
};
|
||||
|
||||
const scheduleAnchorCapture = () => {
|
||||
const el = elementRef.value;
|
||||
// Native scrollend is the primary path; debounce is only a fallback.
|
||||
if (!el || 'onscrollend' in el || !unhooked.value) return;
|
||||
if (anchorDebounceTimeout !== null) {
|
||||
clearTimeout(anchorDebounceTimeout);
|
||||
}
|
||||
anchorDebounceTimeout = setTimeout(() => {
|
||||
anchorDebounceTimeout = null;
|
||||
requestAnchorCapture();
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const handleScrollEnd = () => {
|
||||
if (anchorDebounceTimeout !== null) {
|
||||
clearTimeout(anchorDebounceTimeout);
|
||||
anchorDebounceTimeout = null;
|
||||
}
|
||||
requestAnchorCapture();
|
||||
};
|
||||
|
||||
const restoreAnchor = (el: HTMLElement) => {
|
||||
if (!anchor || !el.contains(anchor.element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const top = el.getBoundingClientRect().top;
|
||||
const delta =
|
||||
anchor.element.getBoundingClientRect().top - top - anchor.offsetFromTop;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
return;
|
||||
}
|
||||
|
||||
isProgrammatic = true;
|
||||
el.scrollTop += delta;
|
||||
requestAnimationFrame(() => {
|
||||
isProgrammatic = false;
|
||||
});
|
||||
};
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
isProgrammatic = false;
|
||||
// Intent to leave bottom — unhook before content mutations can re-stick
|
||||
if (event.deltaY < 0) {
|
||||
setUnhooked(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
const el = elementRef.value;
|
||||
if (!el) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = el;
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (distanceFromBottom <= threshold) {
|
||||
if (isUserScrollingUp.value) {
|
||||
isUserScrollingUp.value = false;
|
||||
shouldAutoScroll.value = true;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
isUserScrollingUp.value = true;
|
||||
shouldAutoScroll.value = false;
|
||||
|
||||
const scrollingUp = el.scrollTop < lastScrollTop;
|
||||
lastScrollTop = el.scrollTop;
|
||||
|
||||
if (isProgrammatic) {
|
||||
if (isAtBottom(el)) {
|
||||
setUnhooked(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user is scrolling up, they are deliberately navigating away from the bottom.
|
||||
// Unhook immediately so we never fight the scroll or snap back down.
|
||||
if (scrollingUp) {
|
||||
setUnhooked(true);
|
||||
scheduleAnchorCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only re-hook when scrolling down into the bottom threshold.
|
||||
if (isAtBottom(el)) {
|
||||
setUnhooked(false);
|
||||
return;
|
||||
}
|
||||
scheduleAnchorCapture();
|
||||
};
|
||||
|
||||
let observer: MutationObserver | null = null;
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
watch(elementRef, (newEl, oldEl) => {
|
||||
if (oldEl) {
|
||||
oldEl.removeEventListener('scroll', handleScroll);
|
||||
observer?.disconnect();
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
const followIfHooked = () => {
|
||||
if (unhooked.value) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (newEl) {
|
||||
newEl.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
observer = new MutationObserver(() => {
|
||||
// Only auto-scroll if user hasn't scrolled up and is near bottom
|
||||
if (!isUserScrollingUp.value && shouldAutoScroll.value) {
|
||||
scrollToBottom('instant');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
observer.observe(newEl, {
|
||||
const handleMutation = (records: MutationRecord[]) => {
|
||||
if (unhooked.value || mutationRaf !== null) {
|
||||
return;
|
||||
}
|
||||
// The isolated sizing mirror changes on every keystroke, but cannot
|
||||
// change scroll height. Keep observing real composer content (files,
|
||||
// tool controls, etc.) as well as message content.
|
||||
if (records.every(record => {
|
||||
const target = record.target instanceof Element ? record.target : record.target.parentElement;
|
||||
return target?.closest('[data-chat-measurement]');
|
||||
})) return;
|
||||
mutationRaf = requestAnimationFrame(() => {
|
||||
mutationRaf = null;
|
||||
followIfHooked();
|
||||
});
|
||||
};
|
||||
|
||||
const attach = (el: HTMLElement) => {
|
||||
el.style.overflowAnchor = 'none';
|
||||
lastScrollTop = el.scrollTop;
|
||||
lastWidth = el.clientWidth;
|
||||
setUnhooked(false);
|
||||
|
||||
el.addEventListener('wheel', handleWheel, { passive: true });
|
||||
el.addEventListener('scroll', handleScroll, { passive: true });
|
||||
el.addEventListener('scrollend', handleScrollEnd, { passive: true });
|
||||
|
||||
observer = new MutationObserver(handleMutation);
|
||||
observer.observe(el, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
const current = elementRef.value;
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = current.clientWidth;
|
||||
if (width === lastWidth) {
|
||||
// Height-only growth (streaming layout): stay stuck if hooked
|
||||
if (!unhooked.value) {
|
||||
scrollToBottom('instant');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lastWidth = width;
|
||||
|
||||
if (!unhooked.value) {
|
||||
scrollToBottom('instant');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!anchor) {
|
||||
captureAnchor(current);
|
||||
}
|
||||
restoreAnchor(current);
|
||||
});
|
||||
resizeObserver.observe(el);
|
||||
|
||||
// Content already taller than the viewport on mount — catch up once
|
||||
requestAnimationFrame(() => {
|
||||
if (!unhooked.value) {
|
||||
scrollToBottom('instant');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const detach = (el: HTMLElement | null) => {
|
||||
if (el) {
|
||||
el.removeEventListener('wheel', handleWheel);
|
||||
el.removeEventListener('scroll', handleScroll);
|
||||
el.removeEventListener('scrollend', handleScrollEnd);
|
||||
}
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
if (mutationRaf !== null) {
|
||||
cancelAnimationFrame(mutationRaf);
|
||||
mutationRaf = null;
|
||||
}
|
||||
if (anchorRaf !== null) {
|
||||
cancelAnimationFrame(anchorRaf);
|
||||
anchorRaf = null;
|
||||
}
|
||||
if (anchorDebounceTimeout !== null) {
|
||||
clearTimeout(anchorDebounceTimeout);
|
||||
anchorDebounceTimeout = null;
|
||||
}
|
||||
anchor = null;
|
||||
};
|
||||
|
||||
watch(elementRef, (newEl, oldEl) => {
|
||||
detach(oldEl ?? null);
|
||||
if (newEl) {
|
||||
attach(newEl);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
onUnmounted(() => {
|
||||
elementRef.value?.removeEventListener('scroll', handleScroll);
|
||||
observer?.disconnect();
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
detach(elementRef.value);
|
||||
});
|
||||
|
||||
return {
|
||||
scrollToBottom,
|
||||
isUserScrollingUp, // expose for UI feedback (optional)
|
||||
isUserScrollingUp,
|
||||
shouldAutoScroll,
|
||||
isAtBottom: () => {
|
||||
const el = elementRef.value;
|
||||
return el ? isAtBottom(el) : true;
|
||||
},
|
||||
};
|
||||
}
|
||||
+263
-92
@@ -1,11 +1,20 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { toRaw } from 'vue';
|
||||
import * as schema from '~~/drizzle/schema';
|
||||
import { Err, Ok, type Result } from '~~/types/result';
|
||||
import { buildMessageTree } from '~~/utils/message';
|
||||
import { appendTextDelta, serializeTopicSnapshot } from '~~/utils/topic-sync';
|
||||
|
||||
export type BaseMessage = {
|
||||
content: string;
|
||||
fileIds: string[];
|
||||
files?: {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
url: string;
|
||||
size?: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type ToolCall = typeof schema.toolCalls.$inferSelect
|
||||
@@ -31,7 +40,7 @@ export enum ChatErrorType {
|
||||
Unimplemented,
|
||||
}
|
||||
|
||||
type Event = { type: string; payload: any; timestamp: number };
|
||||
type Event = { type: string; payload: any; timestamp: number; replay?: boolean };
|
||||
|
||||
const TARGET_UPDATES_PER_SECOND = 24;
|
||||
|
||||
@@ -39,6 +48,10 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
let data = useState<Topic & { messages: Message[] } | undefined>('useChat:data', () => undefined);
|
||||
|
||||
let sse: EventSource | undefined;
|
||||
let disposed = false;
|
||||
let connectionAttempt = 0;
|
||||
let reconnectTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
const { promise: connectionReady, resolve: markConnected } = Promise.withResolvers<void>();
|
||||
|
||||
const textDeltaBuffer: Map<string, Map<string, {
|
||||
content: string;
|
||||
@@ -57,7 +70,7 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
for (const [partId, content] of parts) {
|
||||
const part = msg.parts?.find(p => p.id === partId);
|
||||
if (part) {
|
||||
part.content += content.content;
|
||||
part.content = content.content;
|
||||
part.lastUpdatedAt = new Date(content.lastUpdatedAt);
|
||||
}
|
||||
}
|
||||
@@ -86,6 +99,23 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
flushTimeout = setTimeout(flushTextDeltas, 1000 / TARGET_UPDATES_PER_SECOND);
|
||||
};
|
||||
|
||||
const resetTextDeltas = () => {
|
||||
if (flushTimeout !== undefined) clearTimeout(flushTimeout);
|
||||
flushTimeout = undefined;
|
||||
textDeltaBuffer.clear();
|
||||
flushCbs.clear();
|
||||
};
|
||||
|
||||
const scheduleReconnect = (forceSnapshot = false) => {
|
||||
sse?.close();
|
||||
sse = undefined;
|
||||
if (disposed || reconnectTimeout !== undefined) return;
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
reconnectTimeout = undefined;
|
||||
void connectSSE(forceSnapshot);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const processEvent = (event: Event) => {
|
||||
if (!data.value) return;
|
||||
|
||||
@@ -95,12 +125,17 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
case 'MESSAGE_CREATED': {
|
||||
// Push the new message if it doesn't exist (prevents duplicates from HTTP vs SSE)
|
||||
if (!data.value.messages.find(m => m.id === payload.id)) {
|
||||
data.value.messages.push({ ...payload, parts: [] });
|
||||
data.value.messages.push({ ...payload, parts: payload.parts ?? [], attachments: payload.attachments ?? [] });
|
||||
} else {
|
||||
// replace the existing data with the new data
|
||||
const index = data.value.messages.findIndex(m => m.id === payload.id);
|
||||
if (index !== -1) {
|
||||
data.value.messages[index] = { ...payload, parts: [] };
|
||||
if (index !== -1 && !event.replay) {
|
||||
const existing = data.value.messages[index]!;
|
||||
data.value.messages[index] = {
|
||||
...payload,
|
||||
parts: payload.parts ?? [],
|
||||
attachments: payload.attachments ?? existing.attachments ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -123,33 +158,52 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
const msg = data.value.messages.find(m => m.id === payload.messageId);
|
||||
if (msg) {
|
||||
if (!msg.parts) msg.parts = [];
|
||||
if (!msg.parts.some(part => part.id === payload.part.id)) {
|
||||
msg.parts.push(payload.part);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'text-delta': {
|
||||
const msg = data.value.messages.find(m => m.id === payload.messageId);
|
||||
if (!msg) break;
|
||||
const part = msg.parts?.find(part => part.id === payload.partId);
|
||||
if (!part || part.finished) break;
|
||||
|
||||
if (!textDeltaBuffer.has(payload.messageId)) {
|
||||
textDeltaBuffer.set(payload.messageId, new Map());
|
||||
}
|
||||
const messageBuffer = textDeltaBuffer.get(payload.messageId)!;
|
||||
const existing = messageBuffer.get(payload.partId)?.content || '';
|
||||
messageBuffer.set(payload.partId, { content: existing + payload.content, lastUpdatedAt: payload.lastUpdatedAt });
|
||||
const existing = messageBuffer.get(payload.partId)?.content ?? part.content ?? '';
|
||||
if (typeof payload.offset === 'number' && payload.offset > existing.length) {
|
||||
scheduleReconnect(true);
|
||||
break;
|
||||
}
|
||||
const content = appendTextDelta(existing, payload.content, payload.offset);
|
||||
if (content === existing) break;
|
||||
messageBuffer.set(payload.partId, { content, lastUpdatedAt: payload.lastUpdatedAt });
|
||||
|
||||
scheduleFlush();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'text-delete': {
|
||||
const msg = data.value.messages.find(m => m.id === payload.messageId);
|
||||
if (msg?.parts) msg.parts = msg.parts.filter(part => part.id !== payload.partId);
|
||||
textDeltaBuffer.get(payload.messageId)?.delete(payload.partId);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'text-end': {
|
||||
const msg = data.value.messages.find(m => m.id === payload.messageId);
|
||||
const part = msg?.parts?.find(p => p.id === payload.partId);
|
||||
if (part) {
|
||||
if (part && part.type !== 'tool-call') {
|
||||
nextFlush(() => {
|
||||
part.content = payload.content;
|
||||
if (payload.lastUpdatedAt) {
|
||||
part.lastUpdatedAt = payload.lastUpdatedAt;
|
||||
}
|
||||
part.finished = true;
|
||||
});
|
||||
}
|
||||
@@ -159,8 +213,16 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
case 'tool-call-start': {
|
||||
const msg = data.value.messages.find(m => m.id === payload.messageId);
|
||||
if (msg) {
|
||||
if (!msg.parts) msg.parts = [];
|
||||
msg.parts.push(payload.part);
|
||||
if (!msg.parts) {
|
||||
msg.parts = [];
|
||||
}
|
||||
const part = { ...payload.part };
|
||||
if (!part.createdAt) {
|
||||
part.createdAt = new Date().toISOString();
|
||||
}
|
||||
if (!msg.parts.some(existing => existing.id === part.id)) {
|
||||
msg.parts.push(part);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -169,10 +231,22 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
const msg = data.value.messages.find(m => m.id === payload.messageId);
|
||||
const part = msg?.parts?.find(p => p.toolCallId === payload.toolCallId);
|
||||
if (part?.toolCall) {
|
||||
if (payload.input) part.toolCall.input = payload.input;
|
||||
if (payload.output) part.toolCall.output = payload.output;
|
||||
if (payload.error) part.toolCall.error = payload.error;
|
||||
if (payload.status) part.toolCall.status = payload.status;
|
||||
if (payload.input) {
|
||||
part.toolCall.input = payload.input;
|
||||
}
|
||||
if (payload.output) {
|
||||
part.toolCall.output = payload.output;
|
||||
}
|
||||
if (payload.error) {
|
||||
part.toolCall.error = payload.error;
|
||||
}
|
||||
if (payload.status) {
|
||||
part.toolCall.status = payload.status;
|
||||
if (payload.status === 'completed' || payload.status === 'failed') {
|
||||
part.lastUpdatedAt = payload.lastUpdatedAt || new Date().toISOString();
|
||||
part.finished = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -202,55 +276,79 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
}
|
||||
}
|
||||
|
||||
const connectSSE = async () => {
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
|
||||
const connectSSE = async (forceSnapshot = false) => {
|
||||
if (disposed) return;
|
||||
const attempt = ++connectionAttempt;
|
||||
const id = unref(topicId);
|
||||
const lastUpdate = data.value?.messages.at(-1)?.updatedAt || '0';
|
||||
const msgCount = data.value?.messages.length || 0;
|
||||
|
||||
if (sse && sse.readyState !== EventSource.CLOSED) {
|
||||
if (!(new URL(sse.url).pathname.startsWith(`/api/topic/${id}`))) {
|
||||
sse.close();
|
||||
} else {
|
||||
return;
|
||||
sse?.close();
|
||||
if (flushTimeout !== undefined) {
|
||||
clearTimeout(flushTimeout);
|
||||
flushTextDeltas();
|
||||
}
|
||||
let offeredSnapshot: string | undefined;
|
||||
const query = new URLSearchParams();
|
||||
if (!forceSnapshot && data.value?.id === id && globalThis.crypto?.subtle) {
|
||||
try {
|
||||
offeredSnapshot = serializeTopicSnapshot(toRaw(data.value));
|
||||
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(offeredSnapshot));
|
||||
query.set('snapshot', Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''));
|
||||
} catch {
|
||||
// Fingerprinting is an optimization; a full snapshot remains safe.
|
||||
offeredSnapshot = undefined;
|
||||
}
|
||||
}
|
||||
if (disposed || attempt !== connectionAttempt) return;
|
||||
|
||||
sse = new EventSource(`/api/topic/${id}?lastUpdate=${lastUpdate}&count=${msgCount}`);
|
||||
const source = new EventSource(`/api/topic/${id}?${query}`);
|
||||
sse = source;
|
||||
source.addEventListener('error', () => {
|
||||
if (sse === source) scheduleReconnect();
|
||||
});
|
||||
|
||||
sse.addEventListener("error", (e) => {
|
||||
console.error('sse error', e);
|
||||
setTimeout(connectSSE, 1500);
|
||||
})
|
||||
|
||||
sse.addEventListener("message", (e) => {
|
||||
source.addEventListener('message', (e) => {
|
||||
if (disposed || sse !== source) return;
|
||||
const raw = JSON.parse(e.data);
|
||||
const events: Event[] = Array.isArray(raw) ? raw : [raw];
|
||||
|
||||
for (const { type, payload, timestamp } of events) {
|
||||
for (const event of events) {
|
||||
const { type, payload } = event;
|
||||
if (!type || !payload) continue;
|
||||
|
||||
if (type === 'initial_state') {
|
||||
data.value = payload;
|
||||
resolve()
|
||||
if (type === 'synced') {
|
||||
// Optimistic edits can occur while the handshake is in flight.
|
||||
// Only retain the exact state that we offered to the server.
|
||||
if (offeredSnapshot !== serializeTopicSnapshot(toRaw(data.value))) {
|
||||
scheduleReconnect(true);
|
||||
return;
|
||||
}
|
||||
offeredSnapshot = undefined;
|
||||
markConnected();
|
||||
continue;
|
||||
}
|
||||
|
||||
processEvent({ type, payload, timestamp });
|
||||
if (type === 'initial_state') {
|
||||
resetTextDeltas();
|
||||
data.value = payload;
|
||||
offeredSnapshot = undefined;
|
||||
markConnected();
|
||||
continue;
|
||||
}
|
||||
|
||||
processEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
await promise;
|
||||
}
|
||||
await connectionReady;
|
||||
};
|
||||
|
||||
|
||||
if (connect) {
|
||||
onBeforeUnmount(() => {
|
||||
if (flushTimeout) {
|
||||
clearTimeout(flushTimeout);
|
||||
flushTextDeltas();
|
||||
}
|
||||
disposed = true;
|
||||
connectionAttempt++;
|
||||
if (reconnectTimeout !== undefined) clearTimeout(reconnectTimeout);
|
||||
resetTextDeltas();
|
||||
markConnected();
|
||||
if (sse) {
|
||||
console.log('[SSE] Closing connection');
|
||||
sse.close();
|
||||
@@ -263,12 +361,6 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
const { data: ssrData } = await useFetch<Topic & { messages: Message[] }>(`/api/topic/${id}`);
|
||||
data.value = ssrData.value;
|
||||
} else {
|
||||
// if (data.value === undefined) {
|
||||
// const id = unref(topicId);
|
||||
// const { data: ssrData } = await useFetch<Topic & { messages: Message[] }>(`/api/topic/${id}`);
|
||||
// data.value = ssrData.value;
|
||||
// }
|
||||
|
||||
await connectSSE();
|
||||
}
|
||||
}
|
||||
@@ -305,6 +397,7 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
const sendMessage = async (
|
||||
baseMessage: BaseMessage,
|
||||
onRequest?: () => void,
|
||||
role: 'user' | 'assistant' = 'user',
|
||||
): Promise<Result<void, ChatErrorType>> => {
|
||||
const { user } = useAuth();
|
||||
if (!user.value) return Err(ChatErrorType.NoUser);
|
||||
@@ -312,34 +405,94 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
try {
|
||||
const message = {
|
||||
id: nanoid(),
|
||||
role: 'user',
|
||||
content: baseMessage.content,
|
||||
fileIds: baseMessage.fileIds,
|
||||
role,
|
||||
}
|
||||
|
||||
await $fetch(`/api/topic/${unref(topicId)}/message`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
message
|
||||
message: {
|
||||
...message,
|
||||
content: baseMessage.content,
|
||||
fileIds: baseMessage.fileIds,
|
||||
}
|
||||
},
|
||||
onRequest() {
|
||||
if (data.value) {
|
||||
switch (role) {
|
||||
case 'user': {
|
||||
const optimisticAttachments = baseMessage.files?.map(file => ({
|
||||
id: nanoid(),
|
||||
userId: user.value!.id,
|
||||
topicId: unref(topicId),
|
||||
messageId: message.id,
|
||||
fileId: file.id,
|
||||
createdAt: new Date(),
|
||||
file: {
|
||||
id: file.id,
|
||||
userId: user.value!.id,
|
||||
name: file.name,
|
||||
mimeType: file.mimeType,
|
||||
size: file.size ?? 0,
|
||||
url: file.url,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
})) ?? [];
|
||||
|
||||
data.value!.messages.push({
|
||||
topicId: unref(topicId),
|
||||
userId: user.value!.id,
|
||||
// TODO
|
||||
attachments: [],
|
||||
attachments: optimisticAttachments,
|
||||
parts: undefined,
|
||||
generation: null,
|
||||
parentMessageId: null,
|
||||
generationId: null,
|
||||
activeChildId: null,
|
||||
deleted: null,
|
||||
children: [],
|
||||
deleted: false,
|
||||
content: baseMessage.content,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
children: [],
|
||||
...message
|
||||
} as Message);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'assistant': {
|
||||
data.value!.messages.push({
|
||||
topicId: unref(topicId),
|
||||
userId: user.value!.id,
|
||||
// TODO
|
||||
attachments: [],
|
||||
generation: null,
|
||||
parentMessageId: null,
|
||||
generationId: null,
|
||||
activeChildId: null,
|
||||
children: [],
|
||||
deleted: false,
|
||||
content: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
parts: [
|
||||
{
|
||||
id: 'bogus-id',
|
||||
userId: user.value!.id,
|
||||
topicId: unref(topicId),
|
||||
messageId: message.id,
|
||||
type: 'text',
|
||||
content: baseMessage.content,
|
||||
finished: true,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
providerOptions: null,
|
||||
toolCallId: null,
|
||||
toolCall: null,
|
||||
}
|
||||
],
|
||||
...message
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
onRequest?.();
|
||||
},
|
||||
@@ -368,31 +521,11 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
return Err(ChatErrorType.NoAgent);
|
||||
}
|
||||
|
||||
const { settings } = await useUserSettings();
|
||||
|
||||
let rerank = undefined;
|
||||
|
||||
if (agent.config?.search?.rerank && settings.value.systemAssistants.rerank.enabled) {
|
||||
const rerankModel = await useModels().then(m => m.allModels.value.find(m => m.id === settings.value.systemAssistants.rerank.modelId));
|
||||
if (rerankModel) {
|
||||
const rerankProviderApiKeyRes = await getProviderAPIKey(rerankModel.provider);
|
||||
if (rerankProviderApiKeyRes.ok === false) {
|
||||
return rerankProviderApiKeyRes;
|
||||
}
|
||||
const rerankProviderApiKey = rerankProviderApiKeyRes.data;
|
||||
rerank = {
|
||||
modelId: rerankModel.id,
|
||||
providerApiKey: rerankProviderApiKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await $fetch(`/api/topic/${unref(topicId)}/chat`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
modelId: model.id,
|
||||
providerApiKey,
|
||||
rerank,
|
||||
parentMessageId,
|
||||
},
|
||||
});
|
||||
@@ -463,38 +596,76 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
|
||||
}
|
||||
|
||||
const deleteMessagesFocusedDescendant = async (messageId: string): Promise<Result<void, Error>> => {
|
||||
if (!data.value) return Err(new Error('No data'));
|
||||
if (!data.value) {
|
||||
return Err(new Error("No data"));
|
||||
}
|
||||
|
||||
const msg = data.value.messages.find(m => m.id === messageId);
|
||||
if (!msg) return Err(new Error('Message not found'));
|
||||
const msg = data.value.messages.find((m) => {
|
||||
return m.id === messageId;
|
||||
});
|
||||
if (!msg) {
|
||||
return Err(new Error("Message not found"));
|
||||
}
|
||||
|
||||
const previousMessages = data.value!.messages;
|
||||
const previousMessages = data.value.messages;
|
||||
|
||||
const res = await $fetch(`/api/messages/${messageId}`, {
|
||||
method: 'DELETE',
|
||||
method: "DELETE",
|
||||
onRequest() {
|
||||
if (!msg.activeChildId && (msg.parentMessageId === null || msg.parentMessageId === undefined)) {
|
||||
msg.deleted = true;
|
||||
if (!data.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = data.value!.messages.find(m => m.id === msg.parentMessageId);
|
||||
data.value!.messages = data.value!.messages.filter(m => m.id !== msg.id);
|
||||
if (parent?.deleted) {
|
||||
data.value!.messages = data.value!.messages.filter(m => m.id !== parent.id);
|
||||
if (msg.parentMessageId) {
|
||||
const parent = data.value.messages.find((m) => {
|
||||
return m.id === msg.parentMessageId;
|
||||
});
|
||||
data.value.messages = data.value.messages.filter((m) => {
|
||||
return m.id !== msg.id;
|
||||
});
|
||||
if (parent) {
|
||||
const remainingChildren = data.value.messages.filter((m) => {
|
||||
return m.parentMessageId === parent.id;
|
||||
});
|
||||
if (parent.deleted && remainingChildren.length === 0) {
|
||||
data.value.messages = data.value.messages.filter((m) => {
|
||||
return m.id !== parent.id;
|
||||
});
|
||||
} else if (parent.activeChildId === msg.id) {
|
||||
parent.activeChildId = remainingChildren.length > 0
|
||||
? (parent.deleted ? remainingChildren[0]!.id : remainingChildren[remainingChildren.length - 1]!.id)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const children = data.value.messages.filter((m) => {
|
||||
return m.parentMessageId === msg.id;
|
||||
});
|
||||
if (children.length === 0) {
|
||||
data.value.messages = data.value.messages.filter((m) => {
|
||||
return m.id !== msg.id;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
msg.deleted = true;
|
||||
msg.activeChildId = children[0]?.id ?? null;
|
||||
},
|
||||
onResponseError() {
|
||||
data.value!.messages = previousMessages;
|
||||
if (data.value) {
|
||||
data.value.messages = previousMessages;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return Err(new Error('Failed to delete message'));
|
||||
return Err(new Error("Failed to delete message"));
|
||||
}
|
||||
|
||||
return Ok(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
topic,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { attempt } from "~~/types/result";
|
||||
import * as schema from '~~/drizzle/schema';
|
||||
import { resolveFileUrl } from '~~/utils/url';
|
||||
|
||||
export type ResourceFile = typeof schema.files.$inferSelect;
|
||||
|
||||
@@ -118,12 +119,12 @@ export const useResources = async () => {
|
||||
};
|
||||
|
||||
const copyLink = async (file: ResourceFile) => {
|
||||
const url = file.url;
|
||||
const url = resolveFileUrl(file.url);
|
||||
await navigator.clipboard.writeText(url);
|
||||
};
|
||||
|
||||
const downloadFile = (file: ResourceFile) => {
|
||||
const url = file.url;
|
||||
const url = resolveFileUrl(file.url);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = file.name;
|
||||
|
||||
@@ -8,8 +8,6 @@ export const useTokenDropdown = () => {
|
||||
const triggerElement = useState<HTMLElement | null>('token-dropdown:trigger', () => null);
|
||||
|
||||
const open = (generation: Generation, trigger: HTMLElement) => {
|
||||
console.log("open", generation, trigger);
|
||||
|
||||
activeGeneration.value = generation;
|
||||
triggerElement.value = trigger;
|
||||
isOpen.value = true;
|
||||
|
||||
@@ -3,12 +3,13 @@ const persistTimeout = 150;
|
||||
|
||||
export const useTooltip = () => {
|
||||
const activeElement = useState<HTMLElement | null>('tooltip:activeElement', () => null);
|
||||
const activeText = useState<string | null>('tooltip:activeText', () => null);
|
||||
const activeHotkey = useState<string[] | null>('tooltip:activeHotkey', () => null);
|
||||
const isVisible = useState<boolean>('tooltip:isVisible', () => false);
|
||||
const dwellTimeoutId = useState<NodeJS.Timeout | null>('tooltip:dwellTimeoutId', () => null);
|
||||
const closeTimeoutId = useState<NodeJS.Timeout | null>('tooltip:closeTimeout', () => null);
|
||||
|
||||
const show = (el: HTMLElement, hotkey?: string[]) => {
|
||||
const show = (el: HTMLElement, hotkey?: string[], text?: string) => {
|
||||
if (closeTimeoutId.value) {
|
||||
clearTimeout(closeTimeoutId.value);
|
||||
}
|
||||
@@ -16,10 +17,12 @@ export const useTooltip = () => {
|
||||
if (isVisible.value) {
|
||||
activeElement.value = el;
|
||||
activeHotkey.value = hotkey || null;
|
||||
activeText.value = text || null;
|
||||
} else {
|
||||
dwellTimeoutId.value = setTimeout(() => {
|
||||
activeElement.value = el;
|
||||
activeHotkey.value = hotkey || null;
|
||||
activeText.value = text || null;
|
||||
isVisible.value = true;
|
||||
}, dwellTimeout);
|
||||
}
|
||||
@@ -44,6 +47,7 @@ export const useTooltip = () => {
|
||||
return {
|
||||
activeElement: readonly(activeElement),
|
||||
activeHotkey: readonly(activeHotkey),
|
||||
activeText: readonly(activeText),
|
||||
isVisible: readonly(isVisible),
|
||||
show,
|
||||
hide,
|
||||
|
||||
@@ -10,7 +10,7 @@ export const useTopic = () => {
|
||||
FailedToDecryptProviderApiKey,
|
||||
FailedToGenerate,
|
||||
}
|
||||
const autoRename = async (topicId: string): Promise<Result<string, AutoRenameError>> => {
|
||||
const autoRename = async (topicId: string): Promise<Result<void, AutoRenameError>> => {
|
||||
const { settings } = await useUserSettings();
|
||||
|
||||
if (!settings.value.systemAssistants.rename.enabled) {
|
||||
@@ -54,7 +54,7 @@ export const useTopic = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const { agents } = await useAgents();
|
||||
const { patchTopicLocally, refresh } = await useAgents();
|
||||
|
||||
try {
|
||||
// @ts-ignore - excessive stack depth
|
||||
@@ -65,31 +65,20 @@ export const useTopic = () => {
|
||||
providerApiKey,
|
||||
}),
|
||||
onRequest() {
|
||||
const topics = agents.value.flatMap(agent => agent.topics);
|
||||
const topic = topics.find(topic => topic.id === topicId);
|
||||
if (topic) {
|
||||
agents.value = agents.value.map(agent => {
|
||||
return agent.id === topic.agentId ? { ...agent, topics: agent.topics.map(t => t.id === topic.id ? { ...topic, renaming: true } : t) } : agent;
|
||||
});
|
||||
}
|
||||
patchTopicLocally(topicId, { renaming: true });
|
||||
},
|
||||
onRequestError() {
|
||||
const topics = agents.value.flatMap(agent => agent.topics);
|
||||
const topic = topics.find(topic => topic.id === topicId);
|
||||
if (topic) {
|
||||
agents.value = agents.value.map(agent => {
|
||||
return agent.id === topic.agentId ? { ...agent, topics: agent.topics.map(t => t.id === topic.id ? { ...topic, renaming: false } : t) } : agent;
|
||||
});
|
||||
}
|
||||
},
|
||||
}) as { ok: true, renameId: string } | { ok: false, code: string };
|
||||
if (!res.ok) {
|
||||
patchTopicLocally(topicId, { renaming: false });
|
||||
await refresh();
|
||||
console.error('Failed to auto-rename:', res.code);
|
||||
return Err(AutoRenameError.FailedToGenerate);
|
||||
}
|
||||
|
||||
return Ok(res.renameId);
|
||||
return Ok(undefined);
|
||||
} catch (error) {
|
||||
patchTopicLocally(topicId, { renaming: false });
|
||||
await refresh();
|
||||
console.error('Failed to auto-rename:', error);
|
||||
return Err(AutoRenameError.FailedToGenerate);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
export const useUserEvents = () => {
|
||||
const eventSource = ref<EventSource | null>(null);
|
||||
const agents = useState<AgentWithTopics[]>("agents_state", () => []);
|
||||
const isConnected = useState('userEvents:connected', () => false);
|
||||
const lastEventTimestamp = useState<number>('userEvents:lastTimestamp', () => 0);
|
||||
|
||||
const reconnectAttempts = ref(0);
|
||||
const connectionStartTime = ref<number>(0);
|
||||
const pendingReconnect = ref<NodeJS.Timeout | null>(null);
|
||||
let reconciliationTimer: ReturnType<typeof setInterval> | undefined;
|
||||
let reconciling = false;
|
||||
|
||||
const BASE_DELAY = 1000;
|
||||
const MAX_DELAY = 30000;
|
||||
@@ -50,7 +53,6 @@ export const useUserEvents = () => {
|
||||
cleanup();
|
||||
|
||||
connectionStartTime.value = Date.now();
|
||||
reconnectAttempts.value = 0;
|
||||
|
||||
const source = new EventSource('/api/events');
|
||||
eventSource.value = source;
|
||||
@@ -59,6 +61,7 @@ export const useUserEvents = () => {
|
||||
console.log('User events connected');
|
||||
isConnected.value = true;
|
||||
reconnectAttempts.value = 0;
|
||||
void reconcile();
|
||||
};
|
||||
|
||||
source.onerror = () => {
|
||||
@@ -164,20 +167,48 @@ export const useUserEvents = () => {
|
||||
};
|
||||
|
||||
const reconcile = async () => {
|
||||
console.log('Reconciling user state...');
|
||||
const { data } = await useFetch<AgentWithTopics[]>('/api/agents');
|
||||
if (data.value) {
|
||||
const { agents } = await useAgents();
|
||||
agents.value = data.value;
|
||||
if (reconciling) {
|
||||
return;
|
||||
}
|
||||
reconciling = true;
|
||||
try {
|
||||
agents.value = await $fetch<AgentWithTopics[]>("/api/agents");
|
||||
} catch (error) {
|
||||
console.error("Failed to reconcile topics:", error);
|
||||
} finally {
|
||||
reconciling = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resume = () => {
|
||||
if (document.visibilityState !== "visible") {
|
||||
return;
|
||||
}
|
||||
if (!isConnected.value) {
|
||||
reconnectAttempts.value = 0;
|
||||
void connect();
|
||||
}
|
||||
void reconcile();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
connect();
|
||||
void connect();
|
||||
document.addEventListener("visibilitychange", resume);
|
||||
window.addEventListener("online", resume);
|
||||
window.addEventListener("pageshow", resume);
|
||||
reconciliationTimer = setInterval(() => {
|
||||
if (document.visibilityState === "visible" && agents.value.some(agent => agent.topics.some(topic => topic.renaming))) {
|
||||
void reconcile();
|
||||
}
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup();
|
||||
clearInterval(reconciliationTimer);
|
||||
document.removeEventListener("visibilitychange", resume);
|
||||
window.removeEventListener("online", resume);
|
||||
window.removeEventListener("pageshow", resume);
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -18,8 +18,7 @@ useHead({
|
||||
});
|
||||
|
||||
if (import.meta.client) {
|
||||
const { connect } = useUserEvents();
|
||||
connect();
|
||||
useUserEvents();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -28,7 +27,7 @@ if (import.meta.client) {
|
||||
<div class="p-2 w-full h-full max-w-full overflow-x-hidden">
|
||||
<main
|
||||
class="bg-[var(--bg-surface)] flex flex-col h-full w-full border border-solid border-[var(--color-border)] rounded-lg overflow-hidden">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { BaseMessage } from '~/composables/useChat';
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
|
||||
@@ -43,13 +44,29 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
children: [],
|
||||
generationId: null,
|
||||
activeChildId: null,
|
||||
attachments: [],
|
||||
attachments: message.files?.map(file => ({
|
||||
id: nanoid(),
|
||||
userId: user.value!.id,
|
||||
topicId: '',
|
||||
messageId: '',
|
||||
fileId: file.id,
|
||||
createdAt: new Date(),
|
||||
file: {
|
||||
id: file.id,
|
||||
userId: user.value!.id,
|
||||
name: file.name,
|
||||
mimeType: file.mimeType,
|
||||
size: file.size ?? 0,
|
||||
url: file.url,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
})) ?? [],
|
||||
deleted: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
const topic = await createTopic(agent.value!.id);
|
||||
const topic = await createTopic(agent.value!.id, message);
|
||||
if (!topic) throw new Error('Failed to create topic');
|
||||
|
||||
const { sendMessage, startGeneration } = await useChat(topic.id, false);
|
||||
@@ -75,12 +92,34 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
|
||||
return startGeneration(model, undefined, route.params.id as string);
|
||||
};
|
||||
|
||||
const handleAddMessage = async (message: BaseMessage, role: 'user' | 'assistant', _model: ModelWithProvider | null) => {
|
||||
const user = useAuth().user;
|
||||
if (!user) {
|
||||
console.error('No user');
|
||||
return;
|
||||
}
|
||||
|
||||
const topic = await createTopic(agent.value!.id, message);
|
||||
if (!topic) throw new Error('Failed to create topic');
|
||||
|
||||
const { sendMessage } = await useChat(topic.id, false);
|
||||
|
||||
const res = await sendMessage(message, undefined, role);
|
||||
if (res.ok === false) {
|
||||
console.error('Failed to send message:', res.error);
|
||||
return;
|
||||
}
|
||||
|
||||
await navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full w-full max-w-full overflow-x-hidden">
|
||||
<div class="h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)]">
|
||||
<div
|
||||
class="z-5 h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)] bg-[var(--bg-surface)]">
|
||||
<div class="flex items-center gap-2 max-w-full">
|
||||
<button v-if="!sidebarOpen" @click="openSidebar"
|
||||
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
|
||||
@@ -93,7 +132,7 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
</div>
|
||||
|
||||
<div ref="chatPaneWrapper"
|
||||
class="chat-scroll-container justify-center w-full h-full [scrollbar-width:thin] [scrollbar-color:#888_transparent] overflow-y-scroll overflow-x-hidden flex justify-center pt-4">
|
||||
class="chat-scroll-container justify-center w-full flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#888_transparent] overflow-y-scroll overflow-x-hidden flex justify-center pt-4">
|
||||
<div class="chatPane max-w-4xl w-full min-h-full flex flex-col px-4">
|
||||
<div class="w-full px-px flex flex-col flex-grow gap-2"
|
||||
:class="pendingMessage === null ? 'justify-end pb-28' : 'pb-9'">
|
||||
@@ -108,7 +147,8 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
|
||||
<div class="sticky bottom-0 z-10 bg-[var(--bg-surface)] pb-4 w-full rounded-t-2xl">
|
||||
<ChatInput v-model="inputValue" class="[view-transition-name:chat-prompt] duration-150 ease-in-out"
|
||||
:agent="agent" :providers="providers?.filter(p => p.enabled)" @submit="handleSubmit" />
|
||||
:allow-manual-role="true" :agent="agent" :providers="providers?.filter(p => p.enabled)"
|
||||
@submit="handleSubmit" @add-message="handleAddMessage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,8 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-14 flex items-center justify-between px-4">
|
||||
<div class="h-full flex flex-col overflow-hidden pb-4">
|
||||
<div class="h-14 flex items-center justify-between px-4 shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<button v-if="!sidebarOpen" @click="openSidebar"
|
||||
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
|
||||
@@ -64,7 +65,7 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 px-14 w-full h-full">
|
||||
<div class="flex flex-col gap-4 px-4 md:px-14 w-full flex-1 min-h-0">
|
||||
<div class="flex items-center gap-4">
|
||||
<div>
|
||||
<img v-if="agent?.imageUrl" :src="agent.imageUrl" class="w-16 h-16 rounded-full object-cover" />
|
||||
@@ -74,11 +75,12 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
class="placeholder:text-[var(--text-tertiary)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-border)] text-12 p-0"
|
||||
type="text" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 w-full h-full mb-14">
|
||||
<div class="flex flex-col gap-2 w-full flex-1 min-h-0">
|
||||
<label class="text-sm text-[var(--text-secondary)]">System Message</label>
|
||||
<textarea v-model="systemPrompt" placeholder="You are a helpful assistant."
|
||||
class="p-4 w-full h-full resize-none bg-transparent rounded-lg border border-[var(--color-border)]"
|
||||
class="p-4 w-full flex-1 min-h-0 resize-none bg-transparent rounded-lg border border-[var(--color-border)]"
|
||||
@input="changeSystemPrompt"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { type BaseMessage, type Message, ChatErrorType } from '~/composables/useChat';
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
import { buildFocusedMessageTree } from '~~/utils/message';
|
||||
|
||||
const rootStart = Date.now();
|
||||
import { buildFocusedMessageTree, getActiveMessage, getNextActiveChildId } from '~~/utils/message';
|
||||
|
||||
const chatPaneWrapper = ref<HTMLElement | null>(null);
|
||||
const inputValue = ref<BaseMessage>({ content: '', fileIds: [] });
|
||||
const route = useRoute();
|
||||
const { getAgent, deleteTopic } = await useAgents();
|
||||
const { getAgent, deleteTopic, forkTopic } = await useAgents();
|
||||
const { open: sidebarOpen, openSidebar } = useSidebar();
|
||||
const { providers, allModels } = await useModels();
|
||||
const { addShortcut } = useKeyboardShortcuts();
|
||||
@@ -54,17 +52,37 @@ const submitMessage = async (message: BaseMessage, model: ModelWithProvider | nu
|
||||
startGeneration(model);
|
||||
};
|
||||
|
||||
const handleAddMessage = async (message: BaseMessage, role: 'user' | 'assistant', _model: ModelWithProvider | null) => {
|
||||
inputValue.value = { content: '', fileIds: [] };
|
||||
|
||||
const res = await sendMessage(message, async () => {
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
scrollToBottom('instant');
|
||||
});
|
||||
}, role);
|
||||
if (!res.ok) {
|
||||
console.error('Failed to add message:', res.error);
|
||||
const chatInput = document.getElementById('chat') as HTMLInputElement | null;
|
||||
if (chatInput) {
|
||||
inputValue.value = message;
|
||||
nextTick(() => {
|
||||
chatInput.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegenerate = async (message: Message) => {
|
||||
if (!agent.value!.defaultModelId) {
|
||||
console.error('No model selected');
|
||||
return;
|
||||
}
|
||||
|
||||
let messageId: string;
|
||||
if (message.activeChildId && message.children.length > 0) {
|
||||
messageId = message.activeChildId;
|
||||
} else {
|
||||
messageId = message.id;
|
||||
const activeMsg = getActiveMessage(message);
|
||||
if (!activeMsg) {
|
||||
console.error('No active message');
|
||||
return;
|
||||
}
|
||||
|
||||
const model = allModels.value.find(m => m.id === agent.value!.defaultModelId);
|
||||
@@ -73,35 +91,54 @@ const handleRegenerate = async (message: Message) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await regenerateMessage(messageId, buildFocusedMessageTree(topic.value!.messages), model);
|
||||
const res = await regenerateMessage(activeMsg.id, buildFocusedMessageTree(topic.value!.messages), model);
|
||||
if (!res.ok) {
|
||||
console.error('Failed to regenerate message:', ChatErrorType[res.error]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const flatMessages = computed(() => {
|
||||
const messages: Message[] = [];
|
||||
|
||||
for (const message of topic.value?.messages ?? []) {
|
||||
if (!message.deleted) {
|
||||
messages.push(message);
|
||||
}
|
||||
if (message.children.length > 0) {
|
||||
messages.push(...message.children as Message[]);
|
||||
for (const child of message.children) {
|
||||
if (child && !child.deleted) {
|
||||
messages.push(child as Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
});
|
||||
|
||||
const handleDelete = async (rootMessage: Message) => {
|
||||
let messageId: string;
|
||||
if (rootMessage.activeChildId && rootMessage.children.length > 0) {
|
||||
messageId = rootMessage.activeChildId;
|
||||
} else {
|
||||
messageId = rootMessage.id;
|
||||
const handleFork = async (message: Message) => {
|
||||
const activeMsg = getActiveMessage(message);
|
||||
if (!activeMsg) {
|
||||
console.error('No active message');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteMessagesFocusedDescendant(messageId);
|
||||
const newTopicId = await forkTopic(topicId.value, activeMsg.id);
|
||||
if (newTopicId) {
|
||||
await navigateTo(`/agent/${route.params.id}/topic/${newTopicId}`);
|
||||
} else {
|
||||
console.error('Failed to fork topic');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (rootMessage: Message) => {
|
||||
const activeMsg = getActiveMessage(rootMessage);
|
||||
if (!activeMsg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteMessagesFocusedDescendant(activeMsg.id);
|
||||
if (!result.ok) {
|
||||
console.error('Failed to delete message:', result.error);
|
||||
return;
|
||||
@@ -111,21 +148,19 @@ const handleDelete = async (rootMessage: Message) => {
|
||||
await navigateTo(`/agent/${route.params.id}`);
|
||||
deleteTopic(route.params.id as string, topicId.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { scrollToBottom, isAtBottom } = useAutoScroll(chatPaneWrapper);
|
||||
|
||||
const handleResize = () => {
|
||||
const el = chatPaneWrapper.value;
|
||||
if (!el) return;
|
||||
|
||||
const atBottom = (el.scrollHeight - el.scrollTop - el.clientHeight) <= 80;
|
||||
if (!atBottom) return;
|
||||
if (!isAtBottom()) return;
|
||||
|
||||
nextTick().then(() => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollToBottom('instant');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const activeGeneration = computed(() => {
|
||||
if (topic.value === null) return null;
|
||||
@@ -133,8 +168,6 @@ const activeGeneration = computed(() => {
|
||||
return generations?.find((generation) => generation?.status === 'pending') ?? null;
|
||||
});
|
||||
|
||||
const { scrollToBottom } = useAutoScroll(chatPaneWrapper);
|
||||
|
||||
addShortcut(['ctrl', 'alt', 'n'], (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -168,76 +201,39 @@ addShortcut(['ctrl', 'shift', 'c'], (event) => {
|
||||
handleCancel();
|
||||
})
|
||||
|
||||
addShortcut(['alt', '['], async (event) => {
|
||||
const navigateLastMessage = (direction: -1 | 1) => {
|
||||
const lastMessage = topic.value?.messages?.at(-1);
|
||||
if (!lastMessage || lastMessage.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextActiveChildId = getNextActiveChildId(lastMessage, direction);
|
||||
patchMessageLocally(lastMessage.id, { activeChildId: nextActiveChildId });
|
||||
};
|
||||
|
||||
addShortcut(['alt', '['], (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigateLastMessage(-1);
|
||||
});
|
||||
|
||||
const lastMessage = topic.value?.messages?.at(-1);
|
||||
if (lastMessage && lastMessage.children.length > 0) {
|
||||
const currentIdx = lastMessage.children.findIndex(c => c?.id === lastMessage.activeChildId);
|
||||
const prevIdx = Math.max(0, currentIdx);
|
||||
if (currentIdx > 0) {
|
||||
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[prevIdx - 1]!.id });
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addShortcut(['alt', ']'], async (event) => {
|
||||
addShortcut(['ctrl', 'alt', 'arrowleft'], (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigateLastMessage(-1);
|
||||
});
|
||||
|
||||
const lastMessage = topic.value?.messages?.at(-1);
|
||||
if (lastMessage && lastMessage.children.length > 0) {
|
||||
const currentIdx = lastMessage.children.findIndex(c => c?.id === lastMessage.activeChildId);
|
||||
const maxIdx = lastMessage.deleted ? lastMessage.children.length - 1 : lastMessage.children.length;
|
||||
const nextIdx = Math.min(maxIdx, currentIdx + 1);
|
||||
|
||||
if (lastMessage.deleted) {
|
||||
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[nextIdx]?.id ?? null });
|
||||
} else {
|
||||
if (nextIdx === 0) {
|
||||
patchMessageLocally(lastMessage.id, { activeChildId: null });
|
||||
} else {
|
||||
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[nextIdx - 1]?.id ?? null });
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addShortcut(['ctrl', 'alt', 'arrowleft'], async (event) => {
|
||||
const lastMessage = topic.value?.messages?.at(-1);
|
||||
if (lastMessage && lastMessage.children.length > 0) {
|
||||
addShortcut(['alt', ']'], (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigateLastMessage(1);
|
||||
});
|
||||
|
||||
const currentIdx = lastMessage.children.findIndex(c => c?.id === lastMessage.activeChildId);
|
||||
if (currentIdx > 0) {
|
||||
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[currentIdx - 1]!.id });
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addShortcut(['ctrl', 'alt', 'arrowright'], async (event) => {
|
||||
const lastMessage = topic.value?.messages?.at(-1);
|
||||
if (lastMessage && lastMessage.children.length > 0) {
|
||||
addShortcut(['ctrl', 'alt', 'arrowright'], (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const currentIdx = lastMessage.children.findIndex(c => c?.id === lastMessage.activeChildId);
|
||||
const maxIdx = lastMessage.deleted ? lastMessage.children.length - 1 : lastMessage.children.length;
|
||||
const nextIdx = Math.min(maxIdx, currentIdx + 1);
|
||||
|
||||
if (lastMessage.deleted) {
|
||||
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[nextIdx]?.id ?? null });
|
||||
} else {
|
||||
if (nextIdx === 0) {
|
||||
patchMessageLocally(lastMessage.id, { activeChildId: null });
|
||||
} else {
|
||||
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[nextIdx - 1]?.id ?? null });
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
navigateLastMessage(1);
|
||||
});
|
||||
|
||||
addShortcut(['ctrl', 'alt', 'backspace'], async (event) => {
|
||||
event.preventDefault();
|
||||
@@ -257,13 +253,12 @@ const handleCancel = async () => {
|
||||
method: 'POST',
|
||||
});
|
||||
};
|
||||
|
||||
console.log("full page render took", Date.now() - rootStart);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full w-full max-w-full overflow-x-hidden">
|
||||
<div class="h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)]">
|
||||
<div
|
||||
class="z-5 h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)] bg-[var(--bg-surface)]">
|
||||
<div class="flex items-center gap-2 max-w-full">
|
||||
<button v-if="!sidebarOpen" @click="openSidebar"
|
||||
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
|
||||
@@ -277,7 +272,7 @@ console.log("full page render took", Date.now() - rootStart);
|
||||
|
||||
<!-- chat pane -->
|
||||
<div ref="chatPaneWrapper"
|
||||
class="chat-scroll-container justify-center w-full h-full [scrollbar-width:thin] [scrollbar-color:#888_transparent] overflow-y-scroll overflow-x-hidden flex justify-center pt-4">
|
||||
class="chat-scroll-container justify-center w-full flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#888_transparent] overflow-y-scroll overflow-x-hidden flex justify-center pt-4">
|
||||
<div class="chatPane max-w-4xl w-full min-h-full flex flex-col px-4">
|
||||
<div class="w-full px-px flex flex-col flex-grow gap-2 pb-9">
|
||||
<Suspense>
|
||||
@@ -285,16 +280,17 @@ console.log("full page render took", Date.now() - rootStart);
|
||||
<Message v-for="message in topic.messages" :key="message.id" :message="message"
|
||||
@edit="(value) => patchMessageLocally(message.id, { content: value })"
|
||||
@patch="(updates) => patchMessageLocally(message.id, updates)"
|
||||
@delete="handleDelete(message)" @regenerate="handleRegenerate(message)" />
|
||||
@delete="handleDelete(message)" @regenerate="handleRegenerate(message)"
|
||||
@fork="handleFork(message)" />
|
||||
</template>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div class="sticky bottom-0 z-10 bg-[var(--bg-surface)] pb-4 w-full rounded-t-2xl">
|
||||
<ChatInput v-model="inputValue" class="[view-transition-name:chat-prompt] duration-150 ease-in-out"
|
||||
:loading="activeGeneration !== null" :agent="agent"
|
||||
:providers="providers?.filter(p => p.enabled)" @submit="submitMessage" @cancel="handleCancel"
|
||||
@resize="handleResize" />
|
||||
:loading="activeGeneration !== null" :allow-manual-role="true" :agent="agent"
|
||||
:providers="providers?.filter(p => p.enabled)" @submit="submitMessage"
|
||||
@add-message="handleAddMessage" @cancel="handleCancel" @resize="handleResize" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ const handleChatSubmit = async (message: BaseMessage, model: ModelWithProvider |
|
||||
}
|
||||
}
|
||||
|
||||
const topic = await createTopic(targetAgent.id);
|
||||
const topic = await createTopic(targetAgent.id, message);
|
||||
if (!topic) {
|
||||
console.error('Failed to create topic');
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue';
|
||||
import { resolveFileUrl } from '~~/utils/url';
|
||||
|
||||
const { open: sidebarOpen, openSidebar } = useSidebar();
|
||||
const {
|
||||
@@ -88,7 +89,7 @@ const handleRowClick = (e: MouseEvent) => {
|
||||
if (file && isImage(file.mimeType)) {
|
||||
const row = trigger.closest('[data-file-id]') as HTMLElement;
|
||||
previewOrigin.value = row?.getBoundingClientRect() ?? null;
|
||||
previewSrc.value = file.url;
|
||||
previewSrc.value = resolveFileUrl(file.url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
mounted(el: any, binding: any) {
|
||||
el.clickOutsideEvent = (event: Event) => {
|
||||
if (!el.contains(event.target as Node)) {
|
||||
binding.value();
|
||||
binding.value(event);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { unified } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
|
||||
function remarkSplitBlocks() {
|
||||
return (tree: any) => {
|
||||
visit(tree, 'paragraph', (node, index, parent) => {
|
||||
const breakIndex = node.children.findIndex((child: any) => child.type === 'break');
|
||||
|
||||
if (breakIndex !== -1) {
|
||||
const beforeBreak = node.children.slice(0, breakIndex);
|
||||
const afterBreak = node.children.slice(breakIndex + 1);
|
||||
|
||||
node.children = beforeBreak;
|
||||
|
||||
const newParagraph = {
|
||||
type: 'paragraph',
|
||||
children: afterBreak,
|
||||
};
|
||||
|
||||
let rootChildIndex = -1;
|
||||
if (parent.type === 'root') {
|
||||
rootChildIndex = index!;
|
||||
} else {
|
||||
rootChildIndex = tree.children.findIndex((child: any) =>
|
||||
child === parent || (child.children && child.children.includes(node))
|
||||
);
|
||||
|
||||
if (rootChildIndex === -1) {
|
||||
rootChildIndex = tree.children.indexOf(parent);
|
||||
}
|
||||
}
|
||||
|
||||
if (rootChildIndex !== -1) {
|
||||
tree.children.splice(rootChildIndex + 1, 0, newParagraph);
|
||||
}
|
||||
|
||||
return index! + 1;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const remark =
|
||||
unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkMath)
|
||||
.use(remarkBreaks)
|
||||
.use(remarkSplitBlocks)
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeKatex, { output: 'mathml' });
|
||||
|
||||
return {
|
||||
provide: {
|
||||
remark,
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ComarkPlugin } from "comark";
|
||||
import type { SearchSource, StoredToolValue } from "./tool-rendering";
|
||||
import { parseToolValue, readSearchSources } from "./tool-rendering";
|
||||
|
||||
export const citationPlugin: ComarkPlugin = {
|
||||
name: "source-citations",
|
||||
markdownItPlugins: [
|
||||
(markdown) => {
|
||||
markdown.inline.ruler.before("link", "source_citation", (state, silent) => {
|
||||
if (state.linkLevel > 0 || !state.src.startsWith("[[cite:", state.pos)) {
|
||||
return false;
|
||||
}
|
||||
const match = /^\[\[cite:([a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*)\]\]/.exec(state.src.slice(state.pos));
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
if (silent) {
|
||||
return false;
|
||||
}
|
||||
const token = state.push("mdc_inline_component", "source-cite", 0);
|
||||
token.attrSet("ids", match[1] ?? "");
|
||||
state.pos += match[0].length;
|
||||
return true;
|
||||
});
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const collectCitationSources = (
|
||||
parts: readonly { toolCall?: { toolName: string; output?: StoredToolValue } | null }[],
|
||||
): ReadonlyMap<string, SearchSource> => {
|
||||
const sources = new Map<string, SearchSource>();
|
||||
for (const part of parts) {
|
||||
if (part.toolCall?.toolName !== "search") {
|
||||
continue;
|
||||
}
|
||||
const results = readSearchSources(parseToolValue(part.toolCall.output), { deduplicate: false });
|
||||
for (const source of results ?? []) {
|
||||
if (source.sourceId) {
|
||||
sources.set(source.sourceId, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export const getComposerHeight = (
|
||||
scrollHeight: number,
|
||||
lineHeight: number,
|
||||
padding: number,
|
||||
borders: number,
|
||||
borderBox: boolean,
|
||||
): number => {
|
||||
const contentHeight = Math.min(Math.max(scrollHeight - padding, lineHeight * 2), lineHeight * 10);
|
||||
// A stable CSS-pixel boundary avoids 47.999… / 48.01 oscillations.
|
||||
return Math.round(contentHeight + (borderBox ? padding + borders : 0));
|
||||
};
|
||||
@@ -0,0 +1,277 @@
|
||||
import type { MarkdownDocument, Node } from 'comark';
|
||||
import { createSerializedMarkdownParser as createComarkParser } from 'comark';
|
||||
import breaks from 'comark/plugins/breaks';
|
||||
import footnotes from 'comark/plugins/footnotes';
|
||||
import math from 'comark/plugins/math';
|
||||
import { citationPlugin } from './citations';
|
||||
|
||||
export type MarkdownTree = MarkdownDocument;
|
||||
export type MarkdownAstNode = Node;
|
||||
export type MarkdownElementNode = Exclude<MarkdownAstNode, string | [null, ...unknown[]]>;
|
||||
|
||||
const footnoteSyntax = {
|
||||
name: 'footnote-syntax',
|
||||
markdownItPlugins: [
|
||||
(markdown: any) => markdown.inline.ruler.before('link', 'footnote_inline', (state: any, silent: boolean) => {
|
||||
const start = state.pos;
|
||||
if (!state.src.startsWith('[^', start)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const end = state.src.indexOf(']', start + 2);
|
||||
const label = end === -1 ? '' : state.src.slice(start + 2, end);
|
||||
if (end === -1 || label.length === 0 || /\s/.test(label) || silent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.push('mdc_inline_span', 'span', 1);
|
||||
state.push('text', '', 0).content = `^${label}`;
|
||||
state.push('mdc_inline_span', 'span', -1);
|
||||
state.pos = end + 1;
|
||||
return true;
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const parserPlugins = [
|
||||
breaks(),
|
||||
footnoteSyntax,
|
||||
footnotes(),
|
||||
math(),
|
||||
] as const;
|
||||
|
||||
const createConfiguredParser = (autoClose: boolean, citations: boolean) => createComarkParser({
|
||||
registerDefaultPlugins: false,
|
||||
autoClose,
|
||||
plugins: citations ? [...parserPlugins, citationPlugin] : parserPlugins,
|
||||
});
|
||||
|
||||
const protectDollarRunsInLine = (line: string): string => {
|
||||
let result = '';
|
||||
let segment = '';
|
||||
let codeDelimiterLength = 0;
|
||||
|
||||
const flushSegment = () => {
|
||||
result += segment.replace(/\${3,}/g, (run) => run.replaceAll('$', '$'));
|
||||
segment = '';
|
||||
};
|
||||
|
||||
for (let index = 0; index < line.length;) {
|
||||
let delimiterLength = 0;
|
||||
if (line[index] === '`') {
|
||||
while (line[index + delimiterLength] === '`') {
|
||||
delimiterLength += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (codeDelimiterLength !== 0) {
|
||||
result += line.slice(index, index + (delimiterLength || 1));
|
||||
if (delimiterLength === codeDelimiterLength) {
|
||||
codeDelimiterLength = 0;
|
||||
}
|
||||
index += delimiterLength || 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (delimiterLength === 0) {
|
||||
segment += line[index];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
flushSegment();
|
||||
result += line.slice(index, index + delimiterLength);
|
||||
codeDelimiterLength = delimiterLength;
|
||||
index += delimiterLength;
|
||||
}
|
||||
|
||||
flushSegment();
|
||||
return result;
|
||||
};
|
||||
|
||||
const protectDollarRuns = (content: string): string => {
|
||||
// Fast path: most messages contain no dollar runs; avoid line-splitting entirely.
|
||||
if (!content.includes('$')) {
|
||||
return content;
|
||||
}
|
||||
const lines = content.split('\n');
|
||||
let inFence = false;
|
||||
|
||||
return lines.map((line) => {
|
||||
const fence = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
|
||||
if (fence !== null) {
|
||||
inFence = !inFence;
|
||||
return line;
|
||||
}
|
||||
|
||||
if (inFence) {
|
||||
return line;
|
||||
}
|
||||
|
||||
return protectDollarRunsInLine(line);
|
||||
}).join('\n');
|
||||
};
|
||||
|
||||
export const createMarkdownParser = (options: { citations?: boolean } = {}) => {
|
||||
const citations = options.citations === true;
|
||||
const streamingParser = createConfiguredParser(true, citations);
|
||||
const finalParser = createConfiguredParser(false, citations);
|
||||
|
||||
return (content: string, options: { streaming?: boolean } = {}) => {
|
||||
const protectedContent = protectDollarRuns(content);
|
||||
// Do not let auto-close complete an unfinished citation on the model's behalf.
|
||||
const incompleteCitation = citations && /\[\[cite:[a-zA-Z0-9_,\-]*\]?$/.test(protectedContent);
|
||||
if (options.streaming === true && !incompleteCitation) {
|
||||
return streamingParser(protectedContent, { streaming: true });
|
||||
}
|
||||
|
||||
return finalParser(protectedContent, { streaming: false });
|
||||
};
|
||||
};
|
||||
|
||||
export const agentParser = createMarkdownParser({ citations: true });
|
||||
export const userParser = createMarkdownParser({ citations: false });
|
||||
|
||||
export const getNodeTag = (node: MarkdownAstNode): string | null => {
|
||||
if (!Array.isArray(node) || node[0] === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return node[0];
|
||||
};
|
||||
|
||||
export const getNodeAttributes = (node: MarkdownAstNode): Record<string, unknown> => {
|
||||
if (!Array.isArray(node) || node[0] === null || typeof node[1] !== 'object' || node[1] === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return node[1] as Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const getNodeChildren = (node: MarkdownAstNode): MarkdownAstNode[] => {
|
||||
if (!Array.isArray(node) || node[0] === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return node.slice(2) as MarkdownAstNode[];
|
||||
};
|
||||
|
||||
export const textContent = (node: MarkdownAstNode): string => {
|
||||
if (typeof node === 'string') {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (!Array.isArray(node) || node[0] === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getNodeChildren(node).map(textContent).join('');
|
||||
};
|
||||
|
||||
const splitParagraph = (attributes: Record<string, unknown>, children: MarkdownAstNode[]): MarkdownElementNode[] => {
|
||||
const paragraphs: MarkdownElementNode[] = [];
|
||||
let current: MarkdownAstNode[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
if (getNodeTag(child) === 'br') {
|
||||
paragraphs.push(['p', { ...attributes }, ...current] as MarkdownElementNode);
|
||||
current = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
current.push(child);
|
||||
}
|
||||
|
||||
if (current.length > 0 || paragraphs.length === 0) {
|
||||
paragraphs.push(['p', { ...attributes }, ...current] as MarkdownElementNode);
|
||||
}
|
||||
|
||||
return paragraphs;
|
||||
};
|
||||
|
||||
const hasBrChild = (children: MarkdownAstNode[]): boolean =>
|
||||
children.some((child) => getNodeTag(child) === 'br');
|
||||
|
||||
/**
|
||||
* Transform a single element node into its replacement list. Returns a
|
||||
* single-element array holding the ORIGINAL node reference when nothing
|
||||
* changed, so callers can skip all allocation for unaffected subtrees.
|
||||
*/
|
||||
const transformElement = (node: MarkdownElementNode): MarkdownAstNode[] => {
|
||||
const tag = node[0];
|
||||
const children = getNodeChildren(node);
|
||||
const transformedChildren = transformChildren(children);
|
||||
|
||||
if (tag === 'p') {
|
||||
if (!hasBrChild(transformedChildren)) {
|
||||
if (transformedChildren === children) return [node];
|
||||
return [[tag, { ...node[1] }, ...transformedChildren] as MarkdownElementNode];
|
||||
}
|
||||
return splitParagraph({ ...node[1] }, transformedChildren);
|
||||
}
|
||||
|
||||
if ((tag === 'li' || tag === 'blockquote') && hasBrChild(transformedChildren)) {
|
||||
const segments: MarkdownAstNode[][] = [[]];
|
||||
for (const child of transformedChildren) {
|
||||
if (getNodeTag(child) === 'br') {
|
||||
segments.push([]);
|
||||
} else {
|
||||
segments[segments.length - 1]?.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
return [[
|
||||
tag,
|
||||
{ ...node[1] },
|
||||
...segments
|
||||
.filter((segment) => segment.length > 0)
|
||||
.map((segment) => ['p', {}, ...segment] as MarkdownElementNode),
|
||||
] as MarkdownElementNode];
|
||||
}
|
||||
|
||||
if (transformedChildren === children) return [node];
|
||||
return [[tag, { ...node[1] }, ...transformedChildren] as MarkdownElementNode];
|
||||
};
|
||||
|
||||
const transformChildren = (nodes: MarkdownAstNode[]): MarkdownAstNode[] => {
|
||||
let result: MarkdownAstNode[] | null = null;
|
||||
|
||||
for (let index = 0; index < nodes.length; index++) {
|
||||
const node = nodes[index]!;
|
||||
const replacement = typeof node === 'string' || !Array.isArray(node) || node[0] === null
|
||||
? [node]
|
||||
: transformElement(node);
|
||||
|
||||
if (result === null) {
|
||||
if (replacement.length === 1 && replacement[0] === node) continue;
|
||||
result = nodes.slice(0, index);
|
||||
}
|
||||
result.push(...replacement);
|
||||
}
|
||||
|
||||
return result ?? nodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Comark's breaks plugin emits `br` nodes. Promote each hard break to a
|
||||
* sibling paragraph to keep the chat renderer's established layout.
|
||||
*
|
||||
* Copy-on-write: trees without `br` nodes are returned by reference with no
|
||||
* allocation, so the common case costs a single tag scan per node.
|
||||
*/
|
||||
export const splitParagraphBreaks = (tree: MarkdownTree): MarkdownTree => {
|
||||
const nodes = transformChildren(tree.nodes);
|
||||
return nodes === tree.nodes ? tree : { ...tree, nodes };
|
||||
};
|
||||
|
||||
export const toVueAttributes = (attributes: Record<string, unknown>): Record<string, unknown> => {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(attributes)) {
|
||||
if (key !== '$') {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -54,6 +54,9 @@ import {
|
||||
LogoClosedRouter,
|
||||
LogoPoolside,
|
||||
LogoTencent,
|
||||
LogoThinkingMachines,
|
||||
LogoDotsStudio,
|
||||
LogoNexAgi,
|
||||
LogoHuggingFace,
|
||||
} from '#components';
|
||||
import { markRaw } from 'vue';
|
||||
@@ -65,6 +68,18 @@ interface ModelConfig {
|
||||
}
|
||||
|
||||
const MODEL_MAPPINGS: ModelConfig[] = [
|
||||
{
|
||||
Icon: markRaw(LogoNexAgi),
|
||||
keywords: [/nex-agi\//, /\/nex-/],
|
||||
},
|
||||
{
|
||||
Icon: markRaw(LogoDotsStudio),
|
||||
keywords: [/dots-studio/, /^dots/, /\/dots/],
|
||||
},
|
||||
{
|
||||
Icon: markRaw(LogoThinkingMachines),
|
||||
keywords: [/^thinkingmachines\//, /\/linkling/],
|
||||
},
|
||||
{ Icon: markRaw(LogoOpenAI), keywords: [/gpt-3/], props: { type: 'gpt3' } },
|
||||
{ Icon: markRaw(LogoOpenAI), keywords: [/gpt-4/], props: { type: 'gpt4' } },
|
||||
{ Icon: markRaw(LogoOpenAI), keywords: [/gpt-5/], props: { type: 'gpt5' } },
|
||||
@@ -99,7 +114,7 @@ const MODEL_MAPPINGS: ModelConfig[] = [
|
||||
},
|
||||
{
|
||||
Icon: markRaw(LogoGrok),
|
||||
keywords: [/^grok-/, /^x-ai\//],
|
||||
keywords: [/^grok-/, /^x-ai\//, /^~x-ai\/grok-latest$/],
|
||||
},
|
||||
{
|
||||
Icon: markRaw(LogoGemini),
|
||||
@@ -164,7 +179,7 @@ const MODEL_MAPPINGS: ModelConfig[] = [
|
||||
},
|
||||
{
|
||||
Icon: markRaw(LogoZAI),
|
||||
keywords: [/zai-/, /(^|\/|-)?glm-?\d/]
|
||||
keywords: [/zai-/, /(^|\/|-)?glm-?\d/, /^~z-ai\/glm-(flash-)?latest$/]
|
||||
},
|
||||
{
|
||||
Icon: markRaw(LogoGLMV),
|
||||
@@ -196,7 +211,7 @@ const MODEL_MAPPINGS: ModelConfig[] = [
|
||||
},
|
||||
{
|
||||
Icon: markRaw(LogoMeta),
|
||||
keywords: [/llama/, /\/l3/, /meta-llama\//]
|
||||
keywords: [/llama/, /\/l3/, /meta-llama\//, /(^|\/)muse-spark($|-)/, /^meta\//]
|
||||
},
|
||||
{
|
||||
Icon: markRaw(LogoPerplexity),
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
export type StoredToolValue = Readonly<{ type: string; value: string }> | null | undefined;
|
||||
|
||||
export const parseToolValue = (stored: StoredToolValue): unknown => {
|
||||
if (!stored) {
|
||||
return null;
|
||||
}
|
||||
if (stored.type !== "json") {
|
||||
return stored.value;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(stored.value);
|
||||
} catch {
|
||||
// Tool arguments may still be streaming, or come from older records.
|
||||
return stored.value;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatToolValue = (stored: StoredToolValue): string => {
|
||||
const value = parseToolValue(stored);
|
||||
return typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? "";
|
||||
};
|
||||
|
||||
export interface SearchSource {
|
||||
sourceId?: string;
|
||||
title: string;
|
||||
url: string;
|
||||
hostname: string;
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
export const readSearchWarnings = (value: unknown): { engine: string; reason: string }[] => {
|
||||
if (typeof value !== "object" || value === null || !("warnings" in value) || !Array.isArray(value.warnings)) {
|
||||
return [];
|
||||
}
|
||||
return value.warnings.flatMap((warning: unknown) => {
|
||||
if (typeof warning !== "object" || warning === null
|
||||
|| !("engine" in warning) || typeof warning.engine !== "string"
|
||||
|| !("reason" in warning) || typeof warning.reason !== "string") {
|
||||
return [];
|
||||
}
|
||||
return [{ engine: warning.engine, reason: warning.reason }];
|
||||
});
|
||||
};
|
||||
|
||||
export const readSearchSources = (value: unknown, options: { deduplicate?: boolean } = {}): SearchSource[] | null => {
|
||||
// Older tool calls stored the results array directly.
|
||||
if (typeof value === "object" && value !== null && "results" in value) {
|
||||
value = value.results;
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const sources: SearchSource[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of value) {
|
||||
if (typeof item !== "object" || item === null || !("link" in item) || typeof item.link !== "string") {
|
||||
continue;
|
||||
}
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(item.link);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
||||
continue;
|
||||
}
|
||||
url.hash = "";
|
||||
if (options.deduplicate !== false && seen.has(url.href)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(url.href);
|
||||
sources.push({
|
||||
...("sourceId" in item && typeof item.sourceId === "string" ? { sourceId: item.sourceId } : {}),
|
||||
title: "title" in item && typeof item.title === "string" ? item.title : url.hostname,
|
||||
url: url.href,
|
||||
hostname: url.hostname,
|
||||
snippet: "snippet" in item && typeof item.snippet === "string" ? item.snippet : "",
|
||||
});
|
||||
}
|
||||
return sources;
|
||||
};
|
||||
export interface ToolDisplayInfo {
|
||||
icon: string;
|
||||
verb: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export const formatDuration = (start?: Date | string | number | null, end?: Date | string | number | null): string | null => {
|
||||
if (!start || !end) {
|
||||
return null;
|
||||
}
|
||||
const startTime = typeof start === "number" ? start : new Date(start).getTime();
|
||||
const endTime = typeof end === "number" ? end : new Date(end).getTime();
|
||||
if (Number.isNaN(startTime) || Number.isNaN(endTime) || endTime <= startTime) {
|
||||
return null;
|
||||
}
|
||||
const seconds = (endTime - startTime) / 1000;
|
||||
if (seconds < 0.05) {
|
||||
return null;
|
||||
}
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
};
|
||||
|
||||
const extractField = (obj: unknown, fields: string[]): string => {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
return typeof obj === "string" ? obj : "";
|
||||
}
|
||||
for (const field of fields) {
|
||||
if (field in obj) {
|
||||
const val = (obj as Record<string, unknown>)[field];
|
||||
if (typeof val === "string" && val.length > 0) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const DOMAIN_COLORS = [
|
||||
"#eab308", // amber / yellow
|
||||
"#16a34a", // emerald / green
|
||||
"#0284c7", // sky / blue
|
||||
"#ea580c", // orange
|
||||
"#dc2626", // red
|
||||
"#7c3aed", // violet
|
||||
"#0d9488", // teal
|
||||
"#db2777", // pink
|
||||
"#4f46e5", // indigo
|
||||
];
|
||||
|
||||
export const getDomainColor = (domain: string): string => {
|
||||
const clean = domain.replace(/^www\./, "").toLowerCase();
|
||||
let hash = 0;
|
||||
for (let i = 0; i < clean.length; i++) {
|
||||
hash = (hash << 5) - hash + clean.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
const index = Math.abs(hash) % DOMAIN_COLORS.length;
|
||||
return DOMAIN_COLORS[index] ?? "#eab308";
|
||||
};
|
||||
|
||||
export const domainInitial = (domain: string): string => {
|
||||
const clean = domain.replace(/^www\./, "").trim();
|
||||
return clean ? clean.charAt(0).toUpperCase() : "?";
|
||||
};
|
||||
|
||||
export const getFaviconUrl = (hostname: string): string => {
|
||||
const clean = hostname.replace(/^www\./, "").trim();
|
||||
return `https://icons.duckduckgo.com/ip3/${clean}.ico`;
|
||||
};
|
||||
|
||||
export const getToolDisplayInfo = (toolCall: {
|
||||
toolName: string;
|
||||
status: string;
|
||||
input?: StoredToolValue;
|
||||
output?: StoredToolValue;
|
||||
error?: StoredToolValue;
|
||||
}): ToolDisplayInfo => {
|
||||
const rawInput = parseToolValue(toolCall.input);
|
||||
const rawOutput = parseToolValue(toolCall.output);
|
||||
const rawError = parseToolValue(toolCall.error);
|
||||
|
||||
const isPending = toolCall.status === "pending";
|
||||
const isFailed = toolCall.status === "failed";
|
||||
const isCancelled = toolCall.status === "cancelled";
|
||||
|
||||
const name = toolCall.toolName.toLowerCase();
|
||||
|
||||
const errorSummary = typeof rawError === "string"
|
||||
? (rawError.trim().split("\n")[0] ?? "")
|
||||
: extractField(rawError, ["message", "error"]);
|
||||
|
||||
if (name === "search" || name.includes("search")) {
|
||||
const query = extractField(rawInput, ["query", "q"]);
|
||||
const target = query ? `"${query}"` : "";
|
||||
const icon = "i-mynaui-search";
|
||||
if (isPending) {
|
||||
return { icon, verb: "Searching the web", target };
|
||||
}
|
||||
if (isFailed) {
|
||||
return { icon: "i-mynaui-danger-triangle", verb: "Search failed", target: target || errorSummary };
|
||||
}
|
||||
if (isCancelled) {
|
||||
return { icon, verb: "Search cancelled", target };
|
||||
}
|
||||
const sources = readSearchSources(rawOutput);
|
||||
const count = sources?.length ?? 0;
|
||||
const verb = count > 0 ? `Searched ${count} ${count === 1 ? "site" : "sites"}` : "Searched the web";
|
||||
return { icon, verb, target };
|
||||
}
|
||||
|
||||
if (name === "fetchurl" || name === "fetch_url" || name.includes("fetch") || name.includes("scrape")) {
|
||||
const url = extractField(rawInput, ["url", "link", "uri", "href"]);
|
||||
const icon = "i-mynaui-link";
|
||||
if (isPending) {
|
||||
return { icon, verb: "Fetching URL", target: url };
|
||||
}
|
||||
if (isFailed) {
|
||||
return { icon: "i-mynaui-danger-triangle", verb: "Fetch failed", target: url || errorSummary };
|
||||
}
|
||||
if (isCancelled) {
|
||||
return { icon, verb: "Fetch cancelled", target: url };
|
||||
}
|
||||
return { icon, verb: "Fetched URL", target: url };
|
||||
}
|
||||
|
||||
if (name === "python" || name.includes("python") || name.includes("pyeval")) {
|
||||
const code = extractField(rawInput, ["code", "script", "command"]);
|
||||
const firstLine = code.trim().split("\n")[0] ?? "";
|
||||
const icon = "i-mynaui-terminal";
|
||||
if (isPending) {
|
||||
return { icon, verb: "Running Python", target: firstLine };
|
||||
}
|
||||
if (isFailed) {
|
||||
return { icon: "i-mynaui-danger-triangle", verb: "Python failed", target: firstLine || errorSummary };
|
||||
}
|
||||
if (isCancelled) {
|
||||
return { icon, verb: "Python cancelled", target: firstLine };
|
||||
}
|
||||
return { icon, verb: "Ran Python", target: firstLine };
|
||||
}
|
||||
|
||||
if (name.includes("read") || name.includes("cat") || name.includes("view")) {
|
||||
const path = extractField(rawInput, ["path", "file", "filePath", "filename"]);
|
||||
const icon = "i-mynaui-file";
|
||||
if (isPending) {
|
||||
return { icon, verb: "Reading", target: path };
|
||||
}
|
||||
if (isFailed) {
|
||||
return { icon: "i-mynaui-danger-triangle", verb: "Read failed", target: path || errorSummary };
|
||||
}
|
||||
if (isCancelled) {
|
||||
return { icon, verb: "Read cancelled", target: path };
|
||||
}
|
||||
return { icon, verb: "Read", target: path };
|
||||
}
|
||||
|
||||
if (name.includes("edit") || name.includes("write") || name.includes("modify") || name.includes("patch") || name.includes("save")) {
|
||||
const path = extractField(rawInput, ["path", "file", "filePath", "filename"]);
|
||||
const icon = "i-mynaui-edit";
|
||||
if (isPending) {
|
||||
return { icon, verb: "Editing", target: path };
|
||||
}
|
||||
if (isFailed) {
|
||||
return { icon: "i-mynaui-danger-triangle", verb: "Edit failed", target: path || errorSummary };
|
||||
}
|
||||
if (isCancelled) {
|
||||
return { icon, verb: "Edit cancelled", target: path };
|
||||
}
|
||||
return { icon, verb: "Edited", target: path };
|
||||
}
|
||||
|
||||
if (name.includes("query") || name.includes("sql") || name.includes("database") || name.includes("db")) {
|
||||
const query = extractField(rawInput, ["query", "sql", "statement", "command"]);
|
||||
const icon = "i-mynaui-database";
|
||||
if (isPending) {
|
||||
return { icon, verb: "Querying", target: query };
|
||||
}
|
||||
if (isFailed) {
|
||||
const target = query || errorSummary;
|
||||
return { icon: "i-mynaui-database", verb: "Query failed", target };
|
||||
}
|
||||
if (isCancelled) {
|
||||
return { icon, verb: "Query cancelled", target: query };
|
||||
}
|
||||
return { icon, verb: "Queried", target: query };
|
||||
}
|
||||
|
||||
const formattedName = toolCall.toolName
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.replace(/[-_]/g, " ")
|
||||
.trim();
|
||||
const capitalized = formattedName.charAt(0).toUpperCase() + formattedName.slice(1);
|
||||
|
||||
const genericTarget = extractField(rawInput, ["query", "path", "command", "prompt", "url"])
|
||||
|| (isFailed ? errorSummary : "");
|
||||
|
||||
if (isPending) {
|
||||
return { icon: "i-mynaui-tool", verb: `Running ${capitalized}`, target: genericTarget };
|
||||
}
|
||||
if (isFailed) {
|
||||
return { icon: "i-mynaui-danger-triangle", verb: `${capitalized} failed`, target: genericTarget || errorSummary };
|
||||
}
|
||||
if (isCancelled) {
|
||||
return { icon: "i-mynaui-tool", verb: `${capitalized} cancelled`, target: genericTarget };
|
||||
}
|
||||
return { icon: "i-mynaui-tool", verb: capitalized, target: genericTarget };
|
||||
};
|
||||
@@ -15,34 +15,29 @@
|
||||
"@ai-sdk/openai-compatible": "^2.0.41",
|
||||
"@aws-sdk/client-s3": "^3.1028.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1028.0",
|
||||
"@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@6913",
|
||||
"@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@9489",
|
||||
"@floating-ui/vue": "^1.1.11",
|
||||
"@iconify-json/mynaui": "^1.2.17",
|
||||
"@nuxt/fonts": "0.14.0",
|
||||
"@nuxt/hints": "1.0.0-alpha.5",
|
||||
"@nuxt/icon": "2.2.0",
|
||||
"@openrouter/ai-sdk-provider": "^2.5.1",
|
||||
"@openrouter/ai-sdk-provider": "^2.9.0",
|
||||
"@pydantic/monty": "^0.0.17",
|
||||
"@sentry/nuxt": "^10.48.0",
|
||||
"@tanstack/vue-virtual": "^3.13.23",
|
||||
"ai": "^6.0.156",
|
||||
"ai": "^6.0.182",
|
||||
"ai-sdk-ollama": "^3.8.3",
|
||||
"better-auth": "^1.6.2",
|
||||
"comark": "0.6.2",
|
||||
"comlink": "^4.4.2",
|
||||
"drizzle-orm": "^1.0.0-beta.21",
|
||||
"drizzle-orm": "1.0.0-rc.4-5d5b77c",
|
||||
"glob": "^13.0.6",
|
||||
"katex": "0.17.0",
|
||||
"longcat-ai-sdk-provider": "^0.0.5",
|
||||
"nanoid": "^5.1.7",
|
||||
"nuxt": "^4.4.2",
|
||||
"pg": "^8.20.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"shiki": "^3.23.0",
|
||||
"unified": "^11.0.5",
|
||||
"vue": "^3.6.0-beta.10",
|
||||
"vue-router": "^5.0.4",
|
||||
"zod": "^4.3.6",
|
||||
@@ -77,7 +72,7 @@
|
||||
|
||||
"@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-j3fe/6lUUkHPD/51OgMXN9UD7p1QSQEAlroIinmb3MhJ1s+O0MnqdRa30IM7dRHafNp0FQ9X4YpobY85iMknUQ=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.114", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MqkZ5sd+qiq6RgIxELkoFQXg2/JwK+WCMaot7U+rtrZpWJl3fSyYvc28SC03b256o4F7OXjQtdjTqs81B2w+dA=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.61", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jEKU1Mjcy5CoicejdJQIzM0ntYwyXR8vtYgAZYriKaOuLAiAhiiU538++fGU3CC9HJH/mL1OfsCwMM3gFiCNsw=="],
|
||||
|
||||
@@ -179,20 +174,12 @@
|
||||
|
||||
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
|
||||
|
||||
"@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="],
|
||||
|
||||
"@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
|
||||
|
||||
"@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="],
|
||||
|
||||
"@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="],
|
||||
|
||||
"@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="],
|
||||
|
||||
"@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="],
|
||||
|
||||
"@azure/core-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="],
|
||||
|
||||
"@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg=="],
|
||||
|
||||
"@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="],
|
||||
@@ -201,10 +188,6 @@
|
||||
|
||||
"@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="],
|
||||
|
||||
"@azure/keyvault-common": ["@azure/keyvault-common@2.0.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w=="],
|
||||
|
||||
"@azure/keyvault-keys": ["@azure/keyvault-keys@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag=="],
|
||||
|
||||
"@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="],
|
||||
|
||||
"@azure/msal-browser": ["@azure/msal-browser@4.29.0", "", { "dependencies": { "@azure/msal-common": "15.15.0" } }, "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w=="],
|
||||
@@ -265,9 +248,9 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="],
|
||||
"@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
|
||||
|
||||
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@6913", { "peerDependencies": { "@better-auth/core": "1.5.0-beta.13", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0", "prettier": "^3.7.4" } }],
|
||||
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@9489", { "peerDependencies": { "@better-auth/core": "^1.7.0-beta.3", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }],
|
||||
|
||||
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-YMMm75jek/MNCAFWTAaq/U3VPmFnrwZW4NhBjjAwruHQJEIrSZZaOaUEXuUpFRRBhWqg7OOltQcHMwU/45CkuA=="],
|
||||
|
||||
@@ -279,7 +262,7 @@
|
||||
|
||||
"@better-auth/telemetry": ["@better-auth/telemetry@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-o4gHKXqizUxVUUYChZZTowLEzdsz3ViBE/fKFzfHqNFUnF+aVt8QsbLSfipq1WpTIXyJVT/SnH0hgSdWxdssbQ=="],
|
||||
|
||||
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||
"@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||
|
||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||
|
||||
@@ -405,8 +388,6 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="],
|
||||
|
||||
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
|
||||
|
||||
"@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="],
|
||||
@@ -457,9 +438,9 @@
|
||||
|
||||
"@nuxt/vite-builder": ["@nuxt/vite-builder@4.4.2", "", { "dependencies": { "@nuxt/kit": "4.4.2", "@rollup/plugin-replace": "^6.0.3", "@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue-jsx": "^5.1.4", "autoprefixer": "^10.4.27", "consola": "^3.4.2", "cssnano": "^7.1.3", "defu": "^6.1.4", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "get-port-please": "^3.2.0", "jiti": "^2.6.1", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.1", "mocked-exports": "^0.1.1", "nypm": "^0.6.5", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "postcss": "^8.5.8", "seroval": "^1.5.1", "std-env": "^4.0.0", "ufo": "^1.6.3", "unenv": "^2.0.0-rc.24", "vite": "^7.3.1", "vite-node": "^5.3.0", "vite-plugin-checker": "^0.12.0", "vue-bundle-renderer": "^2.2.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@babel/plugin-syntax-jsx": "^7.25.0", "nuxt": "4.4.2", "rolldown": "^1.0.0-beta.38", "rollup-plugin-visualizer": "^6.0.0 || ^7.0.1", "vue": "^3.3.4" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@babel/plugin-syntax-jsx", "rolldown", "rollup-plugin-visualizer"] }, "sha512-fJaIwMA8ID6BU5EqmoDvnhq4qYDJeWjdHk4jfqy8D3Nm7CoUW0BvX7Ee92XoO05rtUiClGlk/NQ1Ii8hs3ZIbw=="],
|
||||
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.9.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="],
|
||||
|
||||
@@ -977,33 +958,27 @@
|
||||
|
||||
"@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.23", "", { "dependencies": { "@tanstack/virtual-core": "3.13.23" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ=="],
|
||||
|
||||
"@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="],
|
||||
|
||||
"@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
|
||||
|
||||
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
"@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="],
|
||||
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="],
|
||||
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
|
||||
|
||||
"@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="],
|
||||
|
||||
@@ -1013,8 +988,6 @@
|
||||
|
||||
"@types/pg-pool": ["@types/pg-pool@2.0.7", "", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="],
|
||||
|
||||
"@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||
|
||||
"@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="],
|
||||
@@ -1023,7 +996,7 @@
|
||||
|
||||
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
|
||||
|
||||
"@unhead/vue": ["@unhead/vue@2.1.13", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.13" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-HYy0shaHRnLNW9r85gppO8IiGz0ONWVV3zGdlT8CQ0tbTwixznJCIiyqV4BSV1aIF1jJIye0pd1p/k6Eab8Z/A=="],
|
||||
|
||||
@@ -1077,7 +1050,7 @@
|
||||
|
||||
"@vercel/nft": ["@vercel/nft@0.27.10", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0-rc.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
||||
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
|
||||
|
||||
@@ -1171,7 +1144,7 @@
|
||||
|
||||
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
|
||||
|
||||
"ai": ["ai@6.0.156", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uyi/5LYbugHQxZsR2PeAFOZEL4WqKkzZw4pv0nQvvdgxgVOsM7snOmGrYkp5fShxH/vnd08SXvHCVTX7oUW7xQ=="],
|
||||
"ai": ["ai@6.0.182", "", { "dependencies": { "@ai-sdk/gateway": "3.0.114", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ooJdziFjYrYRcsCx107roqA8gDTI3P82nUfroNWIhVvwrkYzEN3W1l50YK+XNqkUew8AiimaW0/SLBewRXMuHQ=="],
|
||||
|
||||
"ai-sdk-ollama": ["ai-sdk-ollama@3.8.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.23", "jsonrepair": "^3.13.3", "ollama": "^0.6.3" }, "peerDependencies": { "ai": "^6.0.154" } }, "sha512-KId/S++eb0CgTPFTtHzCGCrO73kXZLK+hyyZx5k8LVqU2XOEHYKVbIwDiQ+hm3okHjnsGehn4zR4QNm14SUM3Q=="],
|
||||
|
||||
@@ -1197,6 +1170,8 @@
|
||||
|
||||
"are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="],
|
||||
|
||||
"ast-walker-scope": ["ast-walker-scope@0.8.3", "", { "dependencies": { "@babel/parser": "^7.28.4", "ast-kit": "^2.1.3" } }, "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg=="],
|
||||
@@ -1209,8 +1184,6 @@
|
||||
|
||||
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="],
|
||||
|
||||
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
|
||||
@@ -1227,7 +1200,7 @@
|
||||
|
||||
"birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="],
|
||||
|
||||
"bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="],
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
@@ -1261,8 +1234,6 @@
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
@@ -1293,11 +1264,13 @@
|
||||
|
||||
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||
|
||||
"comark": ["comark@0.6.2", "", { "dependencies": { "entities": "^8.0.0", "htmlparser2": "^12.0.0", "js-yaml": "^5.2.1", "markdown-exit": "1.1.0-beta.2" }, "peerDependencies": { "beautiful-mermaid": "^1.1.3", "katex": "^0.17.0", "rangi": "^2.2.0", "shiki": "^4.3.1" }, "optionalPeers": ["beautiful-mermaid", "katex", "rangi", "shiki"] }, "sha512-hlWa7KlGPfRxovQavnHZlLlTUKrfwE7/o4ewCY0FzqFtRvVT8VdslrumQwELIiK9utsurpoSWCBFj1yVMtDOtw=="],
|
||||
|
||||
"comlink": ["comlink@4.4.2", "", {}, "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
|
||||
|
||||
@@ -1355,8 +1328,6 @@
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||
@@ -1389,13 +1360,13 @@
|
||||
|
||||
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
"dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
"domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="],
|
||||
|
||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
"domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="],
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
"domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="],
|
||||
|
||||
"dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
|
||||
|
||||
@@ -1403,7 +1374,7 @@
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@1.0.0-beta.21", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-qG1vkkXPhz9GJ6RZhM/DvzL5jeDNYn35cyGeFA0+8iZQ6GTP4FhbOltNDHh/8XNmcI89b40lXJuH1PdxaA7zdQ=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@1.0.0-beta.21", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-HZcIbVn5J9T/Z91Wj12Pn7Pi8/1aykS/GPJf2lXeZnEuPjxaBfQ+YAt0Sl+XI+9R/D1BpK+2fdIqbpuaTbcvqA=="],
|
||||
"drizzle-orm": ["drizzle-orm@1.0.0-rc.4-5d5b77c", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-Oq9W4B11PracWY9cuCLTFT+JA5kXqR6rBmWcM8oh0xwLgCviVl9wh6ZuWX7Zpv1Jir/8W+M37syvjSYtj1f5iA=="],
|
||||
|
||||
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
|
||||
|
||||
@@ -1425,7 +1396,7 @@
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
"entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
|
||||
|
||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
|
||||
@@ -1469,8 +1440,6 @@
|
||||
|
||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
@@ -1569,30 +1538,16 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
|
||||
|
||||
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
|
||||
|
||||
"hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="],
|
||||
|
||||
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||
|
||||
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
|
||||
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||
|
||||
"hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"htmlparser2": ["htmlparser2@12.0.0", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "domutils": "^4.0.2", "entities": "^8.0.0" } }, "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw=="],
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
@@ -1661,8 +1616,6 @@
|
||||
|
||||
"is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
|
||||
|
||||
"is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
|
||||
@@ -1685,10 +1638,10 @@
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
||||
"js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="],
|
||||
|
||||
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@5.2.3", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.mjs" } }, "sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw=="],
|
||||
|
||||
"jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
@@ -1709,7 +1662,7 @@
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"katex": ["katex@0.16.28", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg=="],
|
||||
"katex": ["katex@0.17.0", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
@@ -1749,6 +1702,8 @@
|
||||
|
||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"linkify-it": ["linkify-it@5.0.2", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q=="],
|
||||
|
||||
"listhen": ["listhen@1.9.0", "", { "dependencies": { "@parcel/watcher": "^2.4.1", "@parcel/watcher-wasm": "^2.4.1", "citty": "^0.1.6", "clipboardy": "^4.0.0", "consola": "^3.2.3", "crossws": ">=0.2.0 <0.4.0", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.12.0", "http-shutdown": "^1.2.2", "jiti": "^2.1.2", "mlly": "^1.7.1", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.5.4", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg=="],
|
||||
|
||||
"loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="],
|
||||
@@ -1783,8 +1738,6 @@
|
||||
|
||||
"longcat-ai-sdk-provider": ["longcat-ai-sdk-provider@0.0.5", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": ">=6.0.0" } }, "sha512-VbuhUuJNXnjoRYnpKBi3TSqFsHJaRRlzjFC+Rv4cxbzgHl6KTcohNeVvgbB95p49lMl4kGH6I/VxgdPOSkAi8A=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
|
||||
"magic-regexp": ["magic-regexp@0.10.0", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.12", "mlly": "^1.7.2", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "ufo": "^1.5.4", "unplugin": "^2.0.0" } }, "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg=="],
|
||||
@@ -1797,96 +1750,24 @@
|
||||
|
||||
"make-fetch-happen": ["make-fetch-happen@9.1.0", "", { "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^6.0.0", "minipass": "^3.1.3", "minipass-collect": "^1.0.2", "minipass-fetch": "^1.3.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.2", "promise-retry": "^2.0.1", "socks-proxy-agent": "^6.0.0", "ssri": "^8.0.0" } }, "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
||||
|
||||
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||
|
||||
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||
|
||||
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||
|
||||
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||
|
||||
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||
|
||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||
|
||||
"mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="],
|
||||
|
||||
"mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="],
|
||||
|
||||
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||
"markdown-exit": ["markdown-exit@1.1.0-beta.2", "", { "dependencies": { "@types/linkify-it": "^5.0.0", "@types/mdurl": "^2.0.0", "entities": "^7.0.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" } }, "sha512-8CzMGVlFZ4DEfnc8KU+4ycUW2SIOuiXqCHD7z51ecVEi/weyc0f2ylQbCm4KoKuVlTZSuMUMnWT0hTyquZ7anQ=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||
|
||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||
|
||||
"mdurl": ["mdurl@2.1.0", "", {}, "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||
|
||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||
|
||||
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||
|
||||
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||
|
||||
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||
|
||||
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||
|
||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||
|
||||
"micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
|
||||
|
||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||
|
||||
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||
|
||||
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||
|
||||
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||
|
||||
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||
|
||||
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||
|
||||
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||
|
||||
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||
|
||||
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||
|
||||
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||
|
||||
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
@@ -1937,8 +1818,6 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="],
|
||||
|
||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||
|
||||
"nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="],
|
||||
@@ -1949,8 +1828,6 @@
|
||||
|
||||
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||
|
||||
"native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
@@ -2005,9 +1882,9 @@
|
||||
|
||||
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
|
||||
|
||||
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
@@ -2029,8 +1906,6 @@
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
@@ -2141,8 +2016,6 @@
|
||||
|
||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
||||
"pretty-bytes": ["pretty-bytes@7.1.0", "", {}, "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
@@ -2155,12 +2028,14 @@
|
||||
|
||||
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||
|
||||
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
@@ -2171,6 +2046,8 @@
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"rangi": ["rangi@2.2.0", "", { "bin": { "rangi": "dist/cli.mjs", "rcat": "dist/cli.mjs" } }, "sha512-zMXLNs+3ejB2dg5kjJdNbllNq6FrdtEbHA9f3POxbSIHb0CyslALbY8qSDCE2r+/6Ku7xaVHKtnC5qHbF2SgNg=="],
|
||||
|
||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||
|
||||
"rc9": ["rc9@3.0.0", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.5" } }, "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA=="],
|
||||
@@ -2193,20 +2070,6 @@
|
||||
|
||||
"regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="],
|
||||
|
||||
"rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="],
|
||||
|
||||
"remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="],
|
||||
@@ -2307,8 +2170,6 @@
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||
|
||||
"sqlite3": ["sqlite3@5.1.7", "", { "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1", "tar": "^6.1.11" }, "optionalDependencies": { "node-gyp": "8.x" } }, "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog=="],
|
||||
|
||||
"srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="],
|
||||
@@ -2367,10 +2228,6 @@
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
|
||||
"tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
|
||||
|
||||
"tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
|
||||
|
||||
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
||||
|
||||
"terser-webpack-plugin": ["terser-webpack-plugin@5.3.16", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q=="],
|
||||
@@ -2397,8 +2254,6 @@
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
||||
@@ -2411,6 +2266,8 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
|
||||
|
||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
|
||||
@@ -2431,8 +2288,6 @@
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="],
|
||||
|
||||
"unimport": ["unimport@6.0.2", "", { "dependencies": { "acorn": "^8.16.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" } }, "sha512-ZSOkrDw380w+KIPniY3smyXh2h7H9v2MNr9zejDuh239o5sdea44DRAYrv+rfUi2QGT186P2h0GPGKvy8avQ5g=="],
|
||||
@@ -2441,14 +2296,10 @@
|
||||
|
||||
"unique-slug": ["unique-slug@2.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w=="],
|
||||
|
||||
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
@@ -2481,8 +2332,6 @@
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vite": ["vite@8.0.0-beta.15", "", { "dependencies": { "@oxc-project/runtime": "0.114.0", "lightningcss": "^1.31.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-rc.5", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RHX7IvsJlEfjyA1rS7MY0UsmF91etdLAamslHR5lfuO3W/BXRdXm2tRE64ztpSPZbKqB4wAAZ0AwtF6QzfKZLA=="],
|
||||
@@ -2513,8 +2362,6 @@
|
||||
|
||||
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
@@ -2567,6 +2414,10 @@
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
|
||||
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q=="],
|
||||
|
||||
"@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q=="],
|
||||
@@ -2603,27 +2454,7 @@
|
||||
|
||||
"@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"@better-auth/core/better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
|
||||
|
||||
"@better-auth/kysely-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
|
||||
|
||||
"@better-auth/kysely-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||
|
||||
"@better-auth/memory-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
|
||||
|
||||
"@better-auth/memory-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||
|
||||
"@better-auth/mongo-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
|
||||
|
||||
"@better-auth/mongo-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||
|
||||
"@better-auth/prisma-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
|
||||
|
||||
"@better-auth/prisma-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||
|
||||
"@better-auth/telemetry/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
|
||||
|
||||
"@better-auth/telemetry/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||
"@bomb.sh/tab/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"@dxup/nuxt/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||
|
||||
@@ -2681,8 +2512,6 @@
|
||||
|
||||
"@nuxt/vite-builder/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
|
||||
|
||||
"@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
|
||||
|
||||
"@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.5.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA=="],
|
||||
@@ -2713,22 +2542,14 @@
|
||||
|
||||
"@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"@sentry/cloudflare/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@sentry/nuxt/@nuxt/kit": ["@nuxt/kit@3.21.1", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-QORZRjcuTKgo++XP1Pc2c2gqwRydkaExrIRfRI9vFsPA3AzuHVn5Gfmbv1ic8y34e78mr5DMBvJlelUaeOuajg=="],
|
||||
|
||||
"@types/connect/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"@types/mssql/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"@types/mysql/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"@types/pg-pool/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
|
||||
|
||||
"@types/readable-stream/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"@types/tedious/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"@typespec/ts-http-runtime/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
@@ -2757,6 +2578,8 @@
|
||||
|
||||
"@vue/compiler-core/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="],
|
||||
|
||||
"@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@vue/compiler-dom/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="],
|
||||
@@ -2787,6 +2610,10 @@
|
||||
|
||||
"@vue/server-renderer/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="],
|
||||
|
||||
"ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
@@ -2799,16 +2626,14 @@
|
||||
|
||||
"ast-walker-scope/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"better-auth/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
|
||||
|
||||
"better-auth/@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-KawrNNuhgmpcc5PgLs6HesMckxCscz5J+BQ99iRmU1cLzG/A87IcydrmYtep+K8WHPN0HmZ/i4z/nOBCtxE2qA=="],
|
||||
|
||||
"better-auth/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||
|
||||
"better-call/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||
|
||||
"better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
|
||||
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"browserslist/caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="],
|
||||
|
||||
"c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||
@@ -2827,9 +2652,11 @@
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
"css-select/domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
"css-select/domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
|
||||
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
@@ -2859,8 +2686,6 @@
|
||||
|
||||
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"listhen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
@@ -2875,6 +2700,8 @@
|
||||
|
||||
"make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"markdown-exit/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
@@ -2913,8 +2740,6 @@
|
||||
|
||||
"nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||
@@ -2939,14 +2764,14 @@
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
|
||||
|
||||
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||
|
||||
"tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"tedious/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||
@@ -2993,6 +2818,8 @@
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@ai-sdk/gateway/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="],
|
||||
|
||||
"@aws-crypto/crc32c/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="],
|
||||
@@ -3013,10 +2840,6 @@
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@better-auth/core/better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
|
||||
"@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||
|
||||
"@dxup/nuxt/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="],
|
||||
@@ -3075,16 +2898,12 @@
|
||||
|
||||
"@types/connect/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"@types/mssql/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"@types/mysql/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"@types/pg-pool/@types/pg/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"@types/pg-pool/@types/pg/pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
|
||||
|
||||
"@types/readable-stream/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"@types/tedious/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"@unocss/cli/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
@@ -3169,6 +2988,8 @@
|
||||
|
||||
"@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"ai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"archiver-utils/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
@@ -3177,6 +2998,12 @@
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"css-select/domhandler/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"css-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"css-select/domutils/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
|
||||
"fontaine/css-tree/mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
||||
@@ -3357,12 +3184,8 @@
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"tar-fs/tar-stream/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"tedious/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||
@@ -3497,8 +3320,6 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@nuxt/kit/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
||||
"@nuxt/kit/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
||||
@@ -3523,14 +3344,20 @@
|
||||
|
||||
"@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@types/pg-pool/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"@unocss/transformer-attributify-jsx/oxc-parser/@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="],
|
||||
|
||||
"@vercel/nft/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"@vue-macros/common/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
@@ -3539,6 +3366,8 @@
|
||||
|
||||
"cacache/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"css-select/domutils/dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"nuxt/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
@@ -3565,16 +3394,20 @@
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"tar-fs/tar-stream/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"unimport/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
||||
"unimport/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
||||
|
||||
"vue-router/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"@nuxt/nitro-server/vue/@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"@nuxt/nitro-server/vue/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@nuxt/nitro-server/vue/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"@vercel/nft/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
@@ -3583,8 +3416,12 @@
|
||||
|
||||
"node-gyp/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"nuxt/vue/@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"nuxt/vue/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"nuxt/vue/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Tool renderers
|
||||
|
||||
Tool presentation lives in `app/components/Message/Agent/Tool/`.
|
||||
|
||||
To add a renderer:
|
||||
|
||||
1. Create a Vue component accepting `toolCall: Readonly<ToolCall>` and optional `part?: MessagePart`.
|
||||
2. Register its exact tool name in `renderers.ts`.
|
||||
3. Wrap your custom presentation with `Disclosure.vue`. Provide `title`, `subtitle`, `icon`, and `customTab` (specifying `id`, `label`, `count`, and optional `copyContent`). Place the custom UI in the named slot matching your tab ID.
|
||||
|
||||
The expanded card seamlessly integrates custom renderer tabs alongside standard inspector tabs (`Input`, `Output`, `Trace`, and `Error` on failure). Line counts and duration are formatted automatically. Built-in `Copy` and `Retry` actions allow quick clipboard access and retry handling. Unknown tools fall back to `Generic.vue` with smart heuristics for common tool signatures (read, edit, query, fetch, python).
|
||||
|
||||
Use `parseToolValue` from `app/utils/tool-rendering.ts` to decode stored values as `unknown`, then validate the tool-specific shape. Inputs and outputs can change while streaming, so derive presentation with computed values and handle pending, completed, failed, and cancelled calls. Never execute tool output as markup or code.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Custom SQL migration file, put your code below! --
|
||||
UPDATE files
|
||||
SET url = regexp_replace(url, '^https?://[^/]+', '')
|
||||
WHERE url ~ '^https?://[^/]+';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
-- Convert naive timestamps to timestamptz.
|
||||
-- Existing values were written as session-local wall clocks (via now()/defaultNow)
|
||||
-- or occasionally as UTC components (drizzle mapToDriverValue toISOString).
|
||||
-- Interpret stored values in the current session TimeZone so local wall-clock
|
||||
-- rows (the common path for messages) keep the same absolute instant.
|
||||
--
|
||||
-- Drizzle reads naive `timestamp` as UTC (textToDateWithTz), which shifted
|
||||
-- displayed times by the server offset (e.g. -5h in America/Chicago).
|
||||
|
||||
ALTER TABLE "providers"
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "models"
|
||||
ALTER COLUMN "released_at" TYPE timestamptz USING "released_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "agents"
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "topics"
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "messages"
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "message_parts"
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "last_updated_at" TYPE timestamptz USING "last_updated_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "tool_calls"
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "files"
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "attachments"
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "users"
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "sessions"
|
||||
ALTER COLUMN "expires_at" TYPE timestamptz USING "expires_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "accounts"
|
||||
ALTER COLUMN "access_token_expires_at" TYPE timestamptz USING "access_token_expires_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "refresh_token_expires_at" TYPE timestamptz USING "refresh_token_expires_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
|
||||
|
||||
ALTER TABLE "verifications"
|
||||
ALTER COLUMN "expires_at" TYPE timestamptz USING "expires_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
|
||||
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "topics" ADD COLUMN "rename_attempt_id" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "topics" ADD COLUMN "rename_expires_at" timestamp with time zone;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "topics" ADD COLUMN "title_source" text DEFAULT 'manual' NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
+36
-31
@@ -11,6 +11,11 @@ import {
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
// Always use timestamptz. Drizzle treats naive `timestamp` values as UTC on read
|
||||
// (via textToDateWithTz), which shifts displayed times by the server offset
|
||||
// (e.g. -5h in America/Chicago).
|
||||
const timestamptz = (name: string) => timestamp(name, { withTimezone: true });
|
||||
|
||||
export const roleEnum = pgEnum('role', ['user', 'assistant']);
|
||||
export const partTypeEnum = pgEnum('part_type', ['reasoning', 'text', 'tool-call', 'file']);
|
||||
export const statusEnum = pgEnum('status', ['pending', 'completed', 'failed', 'cancelled']);
|
||||
@@ -28,10 +33,6 @@ export const settings = pgTable('settings', {
|
||||
enabled: boolean;
|
||||
modelId: string | null;
|
||||
};
|
||||
rerank: {
|
||||
enabled: boolean;
|
||||
modelId: string | null;
|
||||
};
|
||||
}>().notNull().default({} as any),
|
||||
appearance: jsonb('appearance').$type<{
|
||||
colorScheme?: 'light' | 'dark' | 'system';
|
||||
@@ -54,8 +55,8 @@ export const providers = pgTable('providers', {
|
||||
apiKey?: string;
|
||||
apiProxyUrl?: string;
|
||||
}>().default({}),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamptz('updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
|
||||
}, (table) => [
|
||||
index('providers_userId_idx').on(table.userId),
|
||||
]);
|
||||
@@ -89,7 +90,7 @@ export const models = pgTable('models', {
|
||||
supportedParameters: text('supported_parameters').array(),
|
||||
isCustom: boolean('is_custom').notNull().default(false),
|
||||
enabled: boolean('enabled').notNull().default(true),
|
||||
releasedAt: timestamp('released_at'),
|
||||
releasedAt: timestamptz('released_at'),
|
||||
}, (table) => [
|
||||
index('models_providerId_idx').on(table.providerId),
|
||||
index('models_userId_idx').on(table.userId),
|
||||
@@ -105,11 +106,12 @@ export const agents = pgTable('agents', {
|
||||
config: jsonb('config').$type<{
|
||||
search?: {
|
||||
enabled: boolean;
|
||||
rerank: boolean;
|
||||
maxResults: number;
|
||||
}
|
||||
};
|
||||
tools?: {
|
||||
python?: boolean;
|
||||
};
|
||||
}>().default({}),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
}, (table) => [
|
||||
index('agents_userId_idx').on(table.userId),
|
||||
]);
|
||||
@@ -120,7 +122,10 @@ export const topics = pgTable('topics', {
|
||||
agentId: text('agent_id').notNull().references(() => agents.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
renaming: boolean('renaming').default(false),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
renameAttemptId: text('rename_attempt_id'),
|
||||
renameExpiresAt: timestamptz('rename_expires_at'),
|
||||
titleSource: text('title_source').notNull().default('manual'),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
}, (table) => [
|
||||
index('topics_agentId_idx').on(table.agentId),
|
||||
index('topics_userId_idx').on(table.userId),
|
||||
@@ -136,8 +141,8 @@ export const messages = pgTable('messages', {
|
||||
content: text('content'),
|
||||
activeChildId: text('active_child_id'),
|
||||
deleted: boolean('deleted').default(false),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamptz('updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
}, (table) => [
|
||||
index('messages_topicId_idx').on(table.topicId),
|
||||
index('messages_parentMessageId_idx').on(table.parentMessageId),
|
||||
@@ -154,8 +159,8 @@ export const messageParts = pgTable('message_parts', {
|
||||
content: text('content'),
|
||||
providerOptions: jsonb('provider_options'),
|
||||
finished: boolean('finished').notNull().default(false),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
lastUpdatedAt: timestamp('last_updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
lastUpdatedAt: timestamptz('last_updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
|
||||
}, (table) => [
|
||||
index('message_parts_topicId_idx').on(table.topicId),
|
||||
index('message_parts_messageId_idx').on(table.messageId),
|
||||
@@ -179,7 +184,7 @@ export const toolCalls = pgTable('tool_calls', {
|
||||
type: ToolCallType;
|
||||
value: string;
|
||||
}>(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
}, (table) => [
|
||||
index('tool_calls_userId_idx').on(table.userId),
|
||||
]);
|
||||
@@ -214,7 +219,7 @@ export const files = pgTable('files', {
|
||||
mimeType: text('mime_type').notNull(),
|
||||
size: integer('size').notNull().default(0),
|
||||
url: text('url').notNull(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
}, (table) => [
|
||||
index('files_userId_idx').on(table.userId),
|
||||
]);
|
||||
@@ -235,7 +240,7 @@ export const attachments = pgTable('attachments', {
|
||||
topicId: text('topic_id').notNull().references(() => topics.id, { onDelete: 'cascade' }),
|
||||
messageId: text('message_id').notNull().references(() => messages.id, { onDelete: 'cascade' }),
|
||||
fileId: text('file_id').notNull().references(() => files.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
}, (table) => [
|
||||
index('attachments_topicId_idx').on(table.topicId),
|
||||
index('attachments_messageId_idx').on(table.messageId),
|
||||
@@ -248,8 +253,8 @@ export const users = pgTable("users", {
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
createdAt: timestamptz("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamptz("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
@@ -259,10 +264,10 @@ export const sessions = pgTable(
|
||||
"sessions",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
expiresAt: timestamptz("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
createdAt: timestamptz("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamptz("updated_at")
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
@@ -286,12 +291,12 @@ export const accounts = pgTable(
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
accessTokenExpiresAt: timestamptz("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamptz("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
createdAt: timestamptz("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamptz("updated_at")
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
@@ -304,9 +309,9 @@ export const verifications = pgTable(
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
expiresAt: timestamptz("expires_at").notNull(),
|
||||
createdAt: timestamptz("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamptz("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
|
||||
+10
-11
@@ -86,7 +86,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
// shiki
|
||||
// Shiki
|
||||
'shiki/core',
|
||||
'shiki/wasm',
|
||||
'shiki/engine/javascript',
|
||||
@@ -104,23 +104,21 @@ export default defineNuxtConfig({
|
||||
'@shikijs/langs/json',
|
||||
'@shikijs/langs/bash',
|
||||
|
||||
'comark',
|
||||
'comark/plugins/breaks',
|
||||
'comark/plugins/footnotes',
|
||||
'comark/plugins/math',
|
||||
'comlink',
|
||||
'katex',
|
||||
'shiki',
|
||||
|
||||
'@vue/devtools-core',
|
||||
'@vue/devtools-kit',
|
||||
'@sentry/nuxt',
|
||||
'@floating-ui/vue',
|
||||
'@nuxt/hints/runtime/hydration/component',
|
||||
'@tanstack/vue-virtual',
|
||||
'shiki',
|
||||
'better-auth/vue',
|
||||
'unified',
|
||||
'remark-gfm',
|
||||
'remark-parse',
|
||||
'remark-rehype',
|
||||
'remark-math',
|
||||
'remark-breaks',
|
||||
'rehype-katex',
|
||||
'unist-util-visit',
|
||||
'comlink',
|
||||
'nanoid',
|
||||
'drizzle-orm/pg-core'
|
||||
],
|
||||
@@ -169,6 +167,7 @@ export default defineNuxtConfig({
|
||||
|
||||
public: {
|
||||
disableSignup: process.env.DISABLE_SIGNUP === 'true' || process.env.DISABLE_SIGNUP === '1',
|
||||
publicUrl: process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000',
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+6
-11
@@ -20,34 +20,29 @@
|
||||
"@ai-sdk/openai-compatible": "^2.0.41",
|
||||
"@aws-sdk/client-s3": "^3.1028.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1028.0",
|
||||
"@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@6913",
|
||||
"@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@9489",
|
||||
"@floating-ui/vue": "^1.1.11",
|
||||
"@iconify-json/mynaui": "^1.2.17",
|
||||
"@nuxt/fonts": "0.14.0",
|
||||
"@nuxt/hints": "1.0.0-alpha.5",
|
||||
"@nuxt/icon": "2.2.0",
|
||||
"@openrouter/ai-sdk-provider": "^2.5.1",
|
||||
"@openrouter/ai-sdk-provider": "^2.9.0",
|
||||
"@pydantic/monty": "^0.0.17",
|
||||
"@sentry/nuxt": "^10.48.0",
|
||||
"@tanstack/vue-virtual": "^3.13.23",
|
||||
"ai": "^6.0.156",
|
||||
"ai": "^6.0.182",
|
||||
"ai-sdk-ollama": "^3.8.3",
|
||||
"better-auth": "^1.6.2",
|
||||
"comark": "0.6.2",
|
||||
"comlink": "^4.4.2",
|
||||
"drizzle-orm": "^1.0.0-beta.21",
|
||||
"drizzle-orm": "1.0.0-rc.4-5d5b77c",
|
||||
"glob": "^13.0.6",
|
||||
"katex": "0.17.0",
|
||||
"longcat-ai-sdk-provider": "^0.0.5",
|
||||
"nanoid": "^5.1.7",
|
||||
"nuxt": "^4.4.2",
|
||||
"pg": "^8.20.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"shiki": "^3.23.0",
|
||||
"unified": "^11.0.5",
|
||||
"vue": "^3.6.0-beta.10",
|
||||
"vue-router": "^5.0.4",
|
||||
"zod": "^4.3.6"
|
||||
|
||||
@@ -18,8 +18,9 @@ export default defineEventHandler(async (event) => {
|
||||
config: z.object({
|
||||
search: z.object({
|
||||
enabled: z.boolean(),
|
||||
rerank: z.boolean(),
|
||||
maxResults: z.number().min(1).max(50),
|
||||
}).optional(),
|
||||
tools: z.object({
|
||||
python: z.boolean().optional(),
|
||||
}).optional(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { and, eq, isNull, lte, or } from "drizzle-orm";
|
||||
import { topics } from "~~/drizzle/schema";
|
||||
import { db } from "~~/server/lib/db";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
const userId = event.context.user!.id as string;
|
||||
|
||||
// Expired leases also recover jobs interrupted by a server restart.
|
||||
await db.update(topics).set({
|
||||
renaming: false,
|
||||
renameAttemptId: null,
|
||||
renameExpiresAt: null,
|
||||
}).where(and(
|
||||
eq(topics.userId, userId),
|
||||
eq(topics.renaming, true),
|
||||
or(isNull(topics.renameExpiresAt), lte(topics.renameExpiresAt, new Date())),
|
||||
));
|
||||
|
||||
const agents = await db.query.agents.findMany({
|
||||
where: {
|
||||
userId,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { auth } from "~~/lib/auth";
|
||||
import { verifyFileToken } from "~~/server/utils/file-token";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const key = getRouterParam(event, 'key');
|
||||
@@ -6,6 +8,34 @@ export default defineEventHandler(async (event) => {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Missing file key' });
|
||||
}
|
||||
|
||||
const query = getQuery(event);
|
||||
const exp = query.exp ? Number(query.exp) : undefined;
|
||||
const sig = query.sig as string | undefined;
|
||||
|
||||
let authorized = false;
|
||||
|
||||
// Path 1: HMAC token (for AI model access)
|
||||
if (exp && sig && process.env.BETTER_AUTH_SECRET) {
|
||||
authorized = verifyFileToken(key, exp, sig, process.env.BETTER_AUTH_SECRET);
|
||||
}
|
||||
|
||||
// Path 2: Session auth (for client-side access)
|
||||
if (!authorized) {
|
||||
try {
|
||||
const sessionData = await auth.api.getSession(event);
|
||||
if (sessionData) {
|
||||
event.context.user = sessionData.user;
|
||||
authorized = true;
|
||||
}
|
||||
} catch {
|
||||
// No valid session
|
||||
}
|
||||
}
|
||||
|
||||
if (!authorized) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const s3 = new S3Client({
|
||||
@@ -24,14 +54,12 @@ export default defineEventHandler(async (event) => {
|
||||
Key: key,
|
||||
}));
|
||||
|
||||
// 3. Set the correct headers so the browser knows what it's receiving
|
||||
setHeaders(event, {
|
||||
'Content-Type': response.ContentType || 'application/octet-stream',
|
||||
'Content-Length': response.ContentLength?.toString() || '',
|
||||
'Cache-Control': 'public, max-age=3600', // Optional: cache for 1 hour
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
});
|
||||
|
||||
// 4. Return the body as a stream directly to the client
|
||||
return response.Body;
|
||||
} catch (error: any) {
|
||||
if (error.name === 'NoSuchKey') {
|
||||
|
||||
@@ -63,62 +63,106 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
const rootMessage = messagesMap.get(messageId);
|
||||
if (!rootMessage) {
|
||||
const targetMessage = messagesMap.get(messageId);
|
||||
if (!targetMessage) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid message',
|
||||
});
|
||||
}
|
||||
|
||||
if (rootMessage.role === 'user') {
|
||||
await deeplyDeleteMessage(rootMessage.id, topicId, userId);
|
||||
if (targetMessage.role === 'user') {
|
||||
await deeplyDeleteMessage(targetMessage.id, topicId, userId);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (rootMessage.deleted === true) {
|
||||
const childMessage = rootMessage.activeChildId
|
||||
? rootMessage.children.find(c => c.id === rootMessage.activeChildId)
|
||||
: rootMessage.children[0];
|
||||
if (!childMessage) {
|
||||
console.error('Message not found');
|
||||
return { ok: false };
|
||||
if (targetMessage.parentMessageId && messagesMap.has(targetMessage.parentMessageId)) {
|
||||
const parent = messagesMap.get(targetMessage.parentMessageId)!;
|
||||
await deeplyDeleteMessage(targetMessage.id, topicId, userId);
|
||||
|
||||
const remainingChildren = parent.children.filter((child) => {
|
||||
return child.id !== targetMessage.id;
|
||||
});
|
||||
|
||||
if (parent.deleted === true && remainingChildren.length === 0) {
|
||||
await deeplyDeleteMessage(parent.id, topicId, userId);
|
||||
} else if (parent.activeChildId === targetMessage.id) {
|
||||
const nextActiveChildId = remainingChildren.length > 0
|
||||
? (parent.deleted ? remainingChildren[0]!.id : remainingChildren[remainingChildren.length - 1]!.id)
|
||||
: null;
|
||||
await db.update(messages).set({ activeChildId: nextActiveChildId }).where(sql`${messages.id} = ${parent.id} AND ${messages.userId} = ${userId}`);
|
||||
topicEvents.emit(topicId, {
|
||||
type: 'MESSAGE_UPDATED',
|
||||
payload: { ...parent, activeChildId: nextActiveChildId },
|
||||
});
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
await deeplyDeleteMessage(childMessage.id, topicId, userId);
|
||||
if (targetMessage.deleted === true) {
|
||||
const childMessage = targetMessage.activeChildId
|
||||
? targetMessage.children.find((c) => { return c.id === targetMessage.activeChildId; })
|
||||
: targetMessage.children[0];
|
||||
|
||||
const remainingChildren = rootMessage.children.filter(child => child!.id !== childMessage.id);
|
||||
if (childMessage) {
|
||||
await deeplyDeleteMessage(childMessage.id, topicId, userId);
|
||||
const remainingChildren = targetMessage.children.filter((child) => {
|
||||
return child.id !== childMessage.id;
|
||||
});
|
||||
if (remainingChildren.length === 0) {
|
||||
await deeplyDeleteMessage(rootMessage.id, topicId, userId);
|
||||
await deeplyDeleteMessage(targetMessage.id, topicId, userId);
|
||||
} else if (targetMessage.activeChildId === childMessage.id) {
|
||||
const nextActiveChildId = remainingChildren[0]!.id;
|
||||
await db.update(messages).set({ activeChildId: nextActiveChildId }).where(sql`${messages.id} = ${targetMessage.id} AND ${messages.userId} = ${userId}`);
|
||||
topicEvents.emit(topicId, {
|
||||
type: 'MESSAGE_UPDATED',
|
||||
payload: { ...targetMessage, activeChildId: nextActiveChildId },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await deeplyDeleteMessage(targetMessage.id, topicId, userId);
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (
|
||||
rootMessage.activeChildId
|
||||
&& rootMessage.children.length > 0
|
||||
targetMessage.activeChildId
|
||||
&& targetMessage.children.length > 0
|
||||
) {
|
||||
const childMessage = rootMessage.children.find(c => c.id === rootMessage.activeChildId);
|
||||
if (!childMessage) {
|
||||
console.error('Message not found');
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
const childMessage = targetMessage.children.find((c) => {
|
||||
return c.id === targetMessage.activeChildId;
|
||||
});
|
||||
if (childMessage) {
|
||||
await deeplyDeleteMessage(childMessage.id, topicId, userId);
|
||||
const remainingChildren = targetMessage.children.filter((child) => {
|
||||
return child.id !== childMessage.id;
|
||||
});
|
||||
const nextActiveChildId = remainingChildren.length > 0
|
||||
? remainingChildren[remainingChildren.length - 1]!.id
|
||||
: null;
|
||||
await db.update(messages).set({ activeChildId: nextActiveChildId }).where(sql`${messages.id} = ${targetMessage.id} AND ${messages.userId} = ${userId}`);
|
||||
topicEvents.emit(topicId, {
|
||||
type: 'MESSAGE_UPDATED',
|
||||
payload: { ...targetMessage, activeChildId: nextActiveChildId },
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (targetMessage.children.length === 0) {
|
||||
await deeplyDeleteMessage(targetMessage.id, topicId, userId);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (rootMessage.children.length === 0) {
|
||||
await deeplyDeleteMessage(rootMessage.id, topicId, userId);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
await db.update(messages).set({ deleted: true }).where(sql`${messages.id} = ${rootMessage.id} AND ${messages.userId} = ${userId}`);
|
||||
const nextActiveChildId = targetMessage.children[0]?.id ?? null;
|
||||
await db.update(messages).set({
|
||||
deleted: true,
|
||||
activeChildId: nextActiveChildId,
|
||||
}).where(sql`${messages.id} = ${targetMessage.id} AND ${messages.userId} = ${userId}`);
|
||||
|
||||
topicEvents.emit(topicId, {
|
||||
type: 'MESSAGE_UPDATED',
|
||||
payload: { ...rootMessage, deleted: true },
|
||||
payload: { ...targetMessage, deleted: true, activeChildId: nextActiveChildId },
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
|
||||
@@ -22,21 +22,16 @@ export default defineEventHandler(async (event) => {
|
||||
rename: {
|
||||
enabled: false,
|
||||
modelId: null,
|
||||
},
|
||||
rerank: {
|
||||
enabled: false,
|
||||
modelId: null,
|
||||
}
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
const missingSystemAssistants = Object.keys(defaultSystemAssistants).filter(key => !(key in existing!.systemAssistants));
|
||||
if (missingSystemAssistants.length > 0) {
|
||||
if (!("rename" in existing.systemAssistants) || "rerank" in existing.systemAssistants) {
|
||||
const [updated] = await db.update(settings)
|
||||
.set({
|
||||
systemAssistants: {
|
||||
...defaultSystemAssistants,
|
||||
...existing.systemAssistants,
|
||||
rename: existing.systemAssistants.rename ?? defaultSystemAssistants.rename,
|
||||
},
|
||||
})
|
||||
.where(eq(settings.id, existing.id)).returning();
|
||||
|
||||
@@ -12,7 +12,12 @@ const appearanceSchema = z.object({
|
||||
});
|
||||
|
||||
const updateSettingsSchema = z.object({
|
||||
systemAssistants: z.any().optional(),
|
||||
systemAssistants: z.object({
|
||||
rename: z.object({
|
||||
enabled: z.boolean(),
|
||||
modelId: z.string().nullable(),
|
||||
}).partial().optional(),
|
||||
}).optional(),
|
||||
appearance: appearanceSchema.optional(),
|
||||
});
|
||||
|
||||
@@ -53,28 +58,22 @@ export default defineEventHandler(async (event) => {
|
||||
return { ...base, ...incoming };
|
||||
};
|
||||
|
||||
const mergeSystemAssistants = (
|
||||
existingSA: Record<string, unknown> | null,
|
||||
incoming: typeof updates.systemAssistants,
|
||||
): Record<string, unknown> => {
|
||||
const base = existingSA || {};
|
||||
if (!incoming) return base;
|
||||
return { ...base, ...incoming };
|
||||
const newSystemAssistants = {
|
||||
rename: {
|
||||
enabled: updates.systemAssistants?.rename?.enabled ?? existing.systemAssistants.rename?.enabled ?? false,
|
||||
modelId: updates.systemAssistants?.rename?.modelId !== undefined
|
||||
? updates.systemAssistants.rename.modelId
|
||||
: existing.systemAssistants.rename?.modelId ?? null,
|
||||
},
|
||||
};
|
||||
|
||||
const newAppearance = mergeAppearance(
|
||||
existing.appearance as Record<string, unknown> | null,
|
||||
updates.appearance,
|
||||
);
|
||||
const newSystemAssistants = mergeSystemAssistants(
|
||||
existing.systemAssistants as Record<string, unknown> | null,
|
||||
updates.systemAssistants,
|
||||
);
|
||||
|
||||
await db.update(settings)
|
||||
.set({
|
||||
appearance: newAppearance,
|
||||
// @ts-ignore - I dont have the energy to type this correctly right now
|
||||
systemAssistants: newSystemAssistants,
|
||||
})
|
||||
.where(and(eq(settings.id, existingId)));
|
||||
|
||||
@@ -1,44 +1,39 @@
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { topics } from '~~/drizzle/schema';
|
||||
import { db } from '~~/server/lib/db';
|
||||
import { cancelPendingRename } from '~~/server/utils/renames';
|
||||
import { userEvents } from '~~/server/utils/events';
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { topics } from "~~/drizzle/schema";
|
||||
import { db } from "~~/server/lib/db";
|
||||
import { cancelPendingRename } from "~~/server/utils/renames";
|
||||
import { topicEvents, userEvents } from "~~/server/utils/events";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
const topicId = getRouterParam(event, "topicId");
|
||||
const userId = event.context.user?.id;
|
||||
if (!topicId || !userId) {
|
||||
setResponseStatus(event, 400);
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
const topicId = getRouterParam(event, 'topicId')!;
|
||||
|
||||
const success = cancelPendingRename(topicId);
|
||||
if (success) {
|
||||
const res = await db.update(topics).set({
|
||||
const topic = await db.query.topics.findFirst({ where: { id: topicId, userId } });
|
||||
if (!topic) {
|
||||
setResponseStatus(event, 404);
|
||||
return { ok: false };
|
||||
}
|
||||
const cleared = await db.update(topics).set({
|
||||
renaming: false,
|
||||
}).where(and(eq(topics.id, topicId), eq(topics.userId, event.context.user!.id)));
|
||||
if (res.rowCount === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid topic',
|
||||
data: {
|
||||
code: 'INVALID_TOPIC',
|
||||
ok: false,
|
||||
}
|
||||
});
|
||||
renameAttemptId: null,
|
||||
renameExpiresAt: null,
|
||||
}).where(and(
|
||||
eq(topics.id, topicId),
|
||||
eq(topics.userId, userId),
|
||||
topic.renameAttemptId ? eq(topics.renameAttemptId, topic.renameAttemptId) : isNull(topics.renameAttemptId),
|
||||
)).returning({ id: topics.id });
|
||||
if (cleared.length === 0) {
|
||||
return { ok: false };
|
||||
}
|
||||
cancelPendingRename(topicId, topic.renameAttemptId);
|
||||
|
||||
userEvents.emit(topicId, 'topics', {
|
||||
op: 'update',
|
||||
payload: {
|
||||
topicId,
|
||||
renaming: false,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
};
|
||||
const payload = { topicId, renaming: false };
|
||||
userEvents.emit(userId, "topics", { op: "update", payload });
|
||||
topicEvents.emit(topicId, { type: "topic_updated", payload });
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -1,13 +1,16 @@
|
||||
import * as z from 'zod';
|
||||
import { renamePrompt } from '~~/prompts';
|
||||
import { generateText } from 'ai';
|
||||
import { generateText, type UserContent } from "ai";
|
||||
import { nanoid } from "nanoid";
|
||||
import { resolveFileUrl } from "~~/utils/url";
|
||||
import { generateFileToken } from "~~/server/utils/file-token";
|
||||
import { GatewayFetchError, getProviderDetails, type ModelGateway } from '~~/server/utils/ai-provider';
|
||||
import { addPendingRename } from '~~/server/utils/renames';
|
||||
import { addPendingRename, completeRename } from "~~/server/utils/renames";
|
||||
import { userEvents } from '~~/server/utils/events';
|
||||
import { db } from '~~/server/lib/db';
|
||||
import { type Model } from '~/composables/useModels';
|
||||
import { topics } from '~~/drizzle/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { and, eq, gt, isNull, lte, or } from "drizzle-orm";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
@@ -44,6 +47,11 @@ export default defineEventHandler(async (event) => {
|
||||
},
|
||||
with: {
|
||||
parts: true,
|
||||
attachments: {
|
||||
with: {
|
||||
file: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,7 +97,7 @@ export default defineEventHandler(async (event) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (topic.renaming) {
|
||||
if (topic.renaming && topic.renameExpiresAt && topic.renameExpiresAt > new Date()) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Already renaming',
|
||||
@@ -160,37 +168,70 @@ export default defineEventHandler(async (event) => {
|
||||
});
|
||||
}
|
||||
|
||||
const abortController = addPendingRename(topicId);
|
||||
event.waitUntil(autoRename(topicId, abortController, { gateway: gateway.gateway, model }, gateway.textTransformer, firstMessage.content!, userId));
|
||||
const content: UserContent = [{
|
||||
type: "text",
|
||||
text: [firstMessage.content || "Describe the attached content.",
|
||||
...firstMessage.attachments.map(({ file }) => `Attachment: ${file.name}`),
|
||||
].join("\n").slice(0, 12000),
|
||||
}];
|
||||
if (model.inputModalities.includes("image")) {
|
||||
const images = firstMessage.attachments.filter(({ file }) =>
|
||||
file.mimeType.startsWith("image/") && file.size <= 10 * 1024 * 1024,
|
||||
).slice(0, 2);
|
||||
for (const { file } of images) {
|
||||
const url = new URL(resolveFileUrl(file.url));
|
||||
if (file.url.startsWith("/api/files/")) {
|
||||
const secret = process.env.BETTER_AUTH_SECRET;
|
||||
if (!secret) {
|
||||
continue;
|
||||
}
|
||||
const { exp, sig } = generateFileToken(file.url.slice("/api/files/".length), secret);
|
||||
url.searchParams.set("exp", String(exp));
|
||||
url.searchParams.set("sig", sig);
|
||||
}
|
||||
content.push({ type: "image", image: url, mediaType: file.mimeType });
|
||||
}
|
||||
}
|
||||
|
||||
const attemptId = nanoid();
|
||||
const claimed = await db.update(topics).set({
|
||||
renaming: true,
|
||||
renameAttemptId: attemptId,
|
||||
renameExpiresAt: new Date(Date.now() + 90000),
|
||||
}).where(and(
|
||||
eq(topics.id, topicId),
|
||||
eq(topics.userId, userId),
|
||||
eq(topics.name, topic.name),
|
||||
or(eq(topics.renaming, false), isNull(topics.renameExpiresAt), lte(topics.renameExpiresAt, new Date())),
|
||||
)).returning({ id: topics.id });
|
||||
if (claimed.length === 0) {
|
||||
return { ok: false as const, code: "ALREADY_RENAMING" };
|
||||
}
|
||||
userEvents.emit(userId, "topics", {
|
||||
op: "update", payload: { topicId, renaming: true },
|
||||
});
|
||||
const abortController = addPendingRename(topicId, attemptId);
|
||||
event.waitUntil(autoRename(topicId, attemptId, abortController, { gateway: gateway.gateway, model }, gateway.textTransformer, content, userId));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
ok: true as const,
|
||||
};
|
||||
});
|
||||
|
||||
const autoRename = async (
|
||||
topicId: string,
|
||||
attemptId: string,
|
||||
abortController: AbortController,
|
||||
model: {
|
||||
gateway: ModelGateway,
|
||||
model: Model,
|
||||
},
|
||||
textTransformer: ((text: string) => string) | ((text: string) => string)[] | undefined,
|
||||
prompt: string,
|
||||
content: UserContent,
|
||||
userId: string,
|
||||
) => {
|
||||
const deadline = setTimeout(() => abortController.abort(), 90000);
|
||||
try {
|
||||
await db.update(topics).set({
|
||||
renaming: true,
|
||||
}).where(eq(topics.id, topicId));
|
||||
userEvents.emit(model.model.userId, 'topics', {
|
||||
op: 'update',
|
||||
payload: {
|
||||
topicId,
|
||||
renaming: true,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await generateText({
|
||||
model: model.gateway(model.model.externalId),
|
||||
providerOptions: {
|
||||
@@ -199,10 +240,11 @@ const autoRename = async (
|
||||
}
|
||||
},
|
||||
system: renamePrompt,
|
||||
prompt,
|
||||
messages: [{ role: "user", content }],
|
||||
maxRetries: 0,
|
||||
timeout: 90 * 1000,
|
||||
abortSignal: abortController.signal,
|
||||
})
|
||||
});
|
||||
|
||||
let text = response.text;
|
||||
|
||||
@@ -216,15 +258,30 @@ const autoRename = async (
|
||||
}
|
||||
}
|
||||
|
||||
await db.update(topics).set({
|
||||
text = text.trim().replace(/^['"]|['"]$/g, "").split("\n")[0]?.trim().slice(0, 120) || "";
|
||||
if (!text || abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
const updated = await db.update(topics).set({
|
||||
renaming: false,
|
||||
renameAttemptId: null,
|
||||
renameExpiresAt: null,
|
||||
titleSource: "generated",
|
||||
name: text,
|
||||
}).where(eq(topics.id, topicId));
|
||||
}).where(and(
|
||||
eq(topics.id, topicId),
|
||||
eq(topics.renameAttemptId, attemptId),
|
||||
gt(topics.renameExpiresAt, new Date()),
|
||||
)).returning({ id: topics.id });
|
||||
if (updated.length === 0) {
|
||||
return;
|
||||
}
|
||||
userEvents.emit(model.model.userId, 'topics', {
|
||||
op: 'update',
|
||||
payload: {
|
||||
topicId,
|
||||
name: text,
|
||||
titleSource: "generated",
|
||||
renaming: false,
|
||||
},
|
||||
});
|
||||
@@ -233,29 +290,26 @@ const autoRename = async (
|
||||
payload: {
|
||||
topicId,
|
||||
name: text,
|
||||
titleSource: "generated",
|
||||
renaming: false,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to auto-rename:', error);
|
||||
await db.update(topics).set({
|
||||
renaming: false,
|
||||
}).where(eq(topics.id, topicId));
|
||||
userEvents.emit(model.model.userId, 'topics', {
|
||||
op: 'update',
|
||||
payload: {
|
||||
topicId,
|
||||
renaming: false,
|
||||
},
|
||||
});
|
||||
topicEvents.emit(topicId, {
|
||||
type: 'topic_updated',
|
||||
payload: {
|
||||
topicId,
|
||||
renaming: false,
|
||||
},
|
||||
});
|
||||
if (!abortController.signal.aborted) {
|
||||
console.error("Failed to auto-rename:", error);
|
||||
}
|
||||
} finally {
|
||||
completeRename(topicId);
|
||||
clearTimeout(deadline);
|
||||
completeRename(topicId, attemptId);
|
||||
const cleared = await db.update(topics).set({
|
||||
renaming: false,
|
||||
renameAttemptId: null,
|
||||
renameExpiresAt: null,
|
||||
}).where(and(eq(topics.id, topicId), eq(topics.renameAttemptId, attemptId))).returning({ id: topics.id });
|
||||
if (cleared.length > 0) {
|
||||
const payload = { topicId, renaming: false };
|
||||
userEvents.emit(userId, "topics", { op: "update", payload });
|
||||
topicEvents.emit(topicId, { type: "topic_updated", payload });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ import { db } from "~~/server/lib/db";
|
||||
import * as z from 'zod';
|
||||
import { type MessageEntity } from '~/composables/useChat';
|
||||
import { promises as fs } from 'fs';
|
||||
import { glob } from 'glob';
|
||||
import { isLoopFinished, type ModelMessage, rerank, type RerankingModel, streamText, type StreamTextTransform, Tool, tool } from "ai";
|
||||
import { isLoopFinished, type ModelMessage, streamText, type StreamTextTransform, type Tool, tool } from "ai";
|
||||
import { generations, messageParts, messages, toolCalls, ToolCallType } from "~~/drizzle/schema";
|
||||
import { topicEvents } from "~~/server/utils/events";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -12,7 +11,10 @@ import { type Model } from "~/composables/useModels";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { buildFocusedMessageTree, buildMessageTree, marshallMessages } from "~~/utils/message";
|
||||
import path from "path";
|
||||
import { isRerankingProvider } from "~~/server/utils/ai-provider";
|
||||
import { generateFileToken } from "~~/server/utils/file-token";
|
||||
import { serializeToolError } from "~~/utils/tool-errors";
|
||||
import { readFirecrawlResponse } from "~~/server/utils/firecrawl";
|
||||
import { limitSearchResults, readSearchResponse, searchInputSchema, searchOutputSchema } from "~~/server/utils/search";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
@@ -23,10 +25,6 @@ export default defineEventHandler(async (event) => {
|
||||
const result = await readValidatedBody(event, z.object({
|
||||
parentMessageId: z.string().optional(),
|
||||
modelId: z.string(),
|
||||
rerank: z.object({
|
||||
modelId: z.string(),
|
||||
providerApiKey: z.string().optional(),
|
||||
}).optional(),
|
||||
args: z.record(z.string(), z.any()).optional(),
|
||||
providerApiKey: z.string().optional(),
|
||||
}).safeParse);
|
||||
@@ -38,7 +36,7 @@ export default defineEventHandler(async (event) => {
|
||||
});
|
||||
}
|
||||
|
||||
const { parentMessageId, modelId, rerank: rerankConfig, providerApiKey, args } = result.data;
|
||||
const { parentMessageId, modelId, providerApiKey, args } = result.data;
|
||||
|
||||
const topic = await db.query.topics.findFirst({
|
||||
where: {
|
||||
@@ -114,90 +112,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
const agentSearchConfig = topic.agent.config?.search;
|
||||
let searchParam: false | { config: SearchTheWebConfig } = false;
|
||||
|
||||
if (agentSearchConfig?.enabled) {
|
||||
searchParam = { config: { rerank: false, maxResults: agentSearchConfig.maxResults ?? 10 } };
|
||||
|
||||
if (agentSearchConfig.rerank && rerankConfig) {
|
||||
const rerankModel = await db.query.models.findFirst({
|
||||
where: {
|
||||
id: rerankConfig.modelId,
|
||||
userId,
|
||||
},
|
||||
with: {
|
||||
provider: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (rerankModel === undefined) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid rerank model',
|
||||
data: {
|
||||
code: 'INVALID_RERANK_MODEL',
|
||||
ok: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const rerankProviderDetails = await getProviderDetails(rerankModel.provider, rerankConfig.providerApiKey, rerankModel);
|
||||
if (!rerankProviderDetails.ok) {
|
||||
switch (rerankProviderDetails.error) {
|
||||
case GatewayFetchError.NoProviderApiKey: {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `${rerankModel.provider.type} provider requires an API key`,
|
||||
data: {
|
||||
code: 'NO_RERANK_PROVIDER_API_KEY',
|
||||
ok: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
case GatewayFetchError.NoProviderBaseUrl: {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid provider URL',
|
||||
data: {
|
||||
code: 'BAD_RERANK_PROVIDER_URL',
|
||||
ok: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { gateway: rerankGateway } = rerankProviderDetails.data;
|
||||
if (rerankGateway === null) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Invalid gateway',
|
||||
data: {
|
||||
code: 'INVALID_RERANK_GATEWAY',
|
||||
ok: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isRerankingProvider(rerankGateway.gateway)) {
|
||||
searchParam.config = {
|
||||
rerank: true,
|
||||
maxResults: agentSearchConfig.maxResults ?? 10,
|
||||
model: rerankGateway.gateway.reranking(rerankModel.externalId)
|
||||
};
|
||||
} else {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Invalid rerank model',
|
||||
data: {
|
||||
code: 'INVALID_RERANK_MODEL',
|
||||
ok: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const searchEnabled = topic.agent.config?.search?.enabled === true;
|
||||
|
||||
let agentmessage = await db.transaction(async tx => {
|
||||
const generationId = nanoid();
|
||||
@@ -237,7 +152,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
agentmessage.generationId = generation.id;
|
||||
// @ts-ignore
|
||||
// @ts-ignore - doesnt exist on the type but yeah it does now
|
||||
agentmessage.generation = generation;
|
||||
|
||||
return agentmessage;
|
||||
@@ -311,7 +226,13 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
const topicMessages = marshallMessages(topic.agent, buildFocusedMessageTree(topicMessageTree));
|
||||
const fileTokenSecret = process.env.BETTER_AUTH_SECRET!;
|
||||
const topicMessages = marshallMessages(topic.agent, buildFocusedMessageTree(topicMessageTree), {
|
||||
signFileUrl: (fileKey) => {
|
||||
const { exp, sig } = generateFileToken(fileKey, fileTokenSecret);
|
||||
return `exp=${exp}&sig=${sig}`;
|
||||
},
|
||||
});
|
||||
if (topicMessages.ok === false) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
@@ -324,7 +245,10 @@ export default defineEventHandler(async (event) => {
|
||||
generateResponse(
|
||||
agentmessage as MessageEntity,
|
||||
{ gateway: gateway.gateway, model, parameters: args },
|
||||
searchParam,
|
||||
searchEnabled,
|
||||
{
|
||||
python: topic.agent.config?.tools?.python ?? false,
|
||||
},
|
||||
agentmessage.generationId!,
|
||||
userId,
|
||||
topicId,
|
||||
@@ -353,173 +277,80 @@ const formatPartId = (partType: string, existingId: string) => {
|
||||
};
|
||||
|
||||
const formatToolCallId = (nativeId: string) => {
|
||||
return `veridian__tool-${nativeId}-${nanoid()}`;
|
||||
return `veridian__tool-${nativeId.slice(0, 16)}-${nanoid()}`;
|
||||
};
|
||||
|
||||
const evalPython = async (code: string) => {
|
||||
try {
|
||||
let stdout = ''
|
||||
const printCallback = (_: string, text: string) => {
|
||||
stdout += text
|
||||
const formatPythonValue = (value: unknown): string => {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
const m = new Monty(code)
|
||||
await m.run({ printCallback })
|
||||
return stdout
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value, (_, v) => typeof v === 'bigint' ? v.toString() : v);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const evalPython = async (code: string): Promise<string> => {
|
||||
try {
|
||||
let stdout = '';
|
||||
const printCallback = (_: string, text: string) => {
|
||||
stdout += text;
|
||||
};
|
||||
const m = new Monty(code);
|
||||
const result = m.run({
|
||||
printCallback,
|
||||
limits: {
|
||||
maxDurationSecs: 10,
|
||||
maxMemory: 32 * 1024 * 1024,
|
||||
maxRecursionDepth: 100,
|
||||
},
|
||||
});
|
||||
|
||||
const expressionOutput = formatPythonValue(result);
|
||||
if (stdout && expressionOutput) {
|
||||
return stdout.endsWith('\n')
|
||||
? `${stdout}${expressionOutput}`
|
||||
: `${stdout}\n${expressionOutput}`;
|
||||
}
|
||||
return stdout || expressionOutput || '';
|
||||
} catch (error) {
|
||||
if (error instanceof MontySyntaxError) {
|
||||
console.log('Syntax error:', error.message)
|
||||
} else if (error instanceof MontyRuntimeError) {
|
||||
console.log('Runtime error:', error.message)
|
||||
console.log('Traceback:', error.traceback())
|
||||
} else if (error instanceof MontyTypingError) {
|
||||
console.log('Type error:', error.displayDiagnostics())
|
||||
return `SyntaxError: ${error.message}`;
|
||||
}
|
||||
if (error instanceof MontyRuntimeError) {
|
||||
return error.display('traceback') || `RuntimeError: ${error.message}`;
|
||||
}
|
||||
if (error instanceof MontyTypingError) {
|
||||
return error.display('concise') || `TypeError: ${error.message}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : 'Python execution failed';
|
||||
}
|
||||
};
|
||||
|
||||
interface SearchTheWebRerankedConfig {
|
||||
rerank: true;
|
||||
maxResults: number;
|
||||
model: RerankingModel;
|
||||
}
|
||||
export const searchTheWeb = async (query: string, results: number) => {
|
||||
const searchUrl = new URL(process.env.SEARCH_API_URL ?? process.env.SEARXNG_URL ?? "");
|
||||
searchUrl.pathname = `${searchUrl.pathname.replace(/\/+$/, "")}/search`;
|
||||
|
||||
interface SearchTheWebUnrankedConfig {
|
||||
rerank?: false;
|
||||
maxResults: number;
|
||||
}
|
||||
|
||||
type SearchTheWebConfig = SearchTheWebRerankedConfig | SearchTheWebUnrankedConfig;
|
||||
|
||||
export const searchTheWeb = (config: SearchTheWebConfig) => {
|
||||
return async (query: string) => {
|
||||
const results = await $fetch<{
|
||||
results: Array<{
|
||||
title: string;
|
||||
url: string;
|
||||
content: string;
|
||||
}>;
|
||||
}>(`${process.env.SEARXNG_URL}/search`, {
|
||||
const response = await $fetch<unknown>(searchUrl.toString(), {
|
||||
query: {
|
||||
q: query,
|
||||
format: 'json',
|
||||
}
|
||||
});
|
||||
|
||||
const sites = results.results.map((item: any) => ({
|
||||
title: item.title,
|
||||
link: item.url,
|
||||
snippet: item.content,
|
||||
}));
|
||||
|
||||
if (config.rerank) {
|
||||
const { ranking } = await rerank({
|
||||
model: config.model,
|
||||
query,
|
||||
documents: sites.map(site => site.snippet),
|
||||
});
|
||||
|
||||
const ranked_sites = [];
|
||||
|
||||
for (let i = 0; i < ranking.length; i++) {
|
||||
ranked_sites.push(sites[ranking[i]!.originalIndex]);
|
||||
const result = readSearchResponse(response);
|
||||
if (!result.ok) {
|
||||
// Reject execution so the SDK emits a tool-error rather than an empty success.
|
||||
return Promise.reject(new Error(result.error));
|
||||
}
|
||||
|
||||
return ranked_sites.slice(0, config.maxResults || 10);
|
||||
}
|
||||
|
||||
return sites.slice(0, config.maxResults || 10);
|
||||
};
|
||||
return limitSearchResults(result.data, results);
|
||||
};
|
||||
|
||||
// TODO: obviously come up with a better way for the user to define their own tools
|
||||
const { listDirectoryTool, globTool, readFileTool, readFilesTool, fetchUrlTool, pythonTool, bashTool } = {
|
||||
listDirectoryTool: tool({
|
||||
description: 'Lists the contents of a directory',
|
||||
inputSchema: z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
files: z.array(z.object({ name: z.string(), type: z.string() })),
|
||||
}),
|
||||
execute: async ({ path }) => {
|
||||
const rawFiles = await fs.readdir(path, { withFileTypes: true });
|
||||
const files = rawFiles.map((file) => ({
|
||||
name: file.name,
|
||||
type: file.isFile() ? 'file' : 'directory',
|
||||
}));
|
||||
|
||||
return {
|
||||
files,
|
||||
};
|
||||
},
|
||||
}),
|
||||
globTool: tool({
|
||||
description: 'Lists files matching a glob pattern',
|
||||
inputSchema: z.object({
|
||||
pattern: z.string(),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
files: z.array(z.object({ path: z.string(), type: z.string() })),
|
||||
}),
|
||||
execute: async ({ pattern }) => {
|
||||
const rawFiles = await glob(pattern, { withFileTypes: true });
|
||||
const files = rawFiles.map((file) => ({
|
||||
path: file.parentPath + '/' + file.name,
|
||||
type: file.isFile() ? 'file' : 'directory',
|
||||
}));
|
||||
|
||||
return {
|
||||
files,
|
||||
};
|
||||
},
|
||||
}),
|
||||
readFileTool: tool({
|
||||
description: 'Reads the contents of a file',
|
||||
inputSchema: z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
}),
|
||||
execute: async ({ path }) => {
|
||||
const file = await fs.readFile(path);
|
||||
return {
|
||||
path,
|
||||
content: file.toString(),
|
||||
};
|
||||
},
|
||||
}),
|
||||
readFilesTool: tool({
|
||||
description: 'Reads the contents of multiple files',
|
||||
inputSchema: z.object({
|
||||
paths: z.array(z.string()).describe('a list of file paths to read'),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
files: z.array(
|
||||
z.object({
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
execute: async ({ paths }) => {
|
||||
const files = await Promise.all(
|
||||
paths.map(async (path) => {
|
||||
const file = await fs.readFile(path);
|
||||
return {
|
||||
path: path,
|
||||
content: file.toString(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
files,
|
||||
};
|
||||
},
|
||||
}),
|
||||
fetchUrlTool: tool({
|
||||
const fetchUrlTool = tool({
|
||||
description: 'Fetches the content of a URL',
|
||||
inputSchema: z.object({
|
||||
url: z.string(),
|
||||
@@ -528,17 +359,32 @@ const { listDirectoryTool, globTool, readFileTool, readFilesTool, fetchUrlTool,
|
||||
content: z.string(),
|
||||
}),
|
||||
execute: async ({ url }) => {
|
||||
const response = await fetch(url);
|
||||
const content = await response.text();
|
||||
const response = await fetch('https://api.firecrawl.dev/v2/scrape', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + process.env.FIRECRAWL_API_KEY,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
formats: ["markdown"],
|
||||
}),
|
||||
});
|
||||
const result = await readFirecrawlResponse(response);
|
||||
if (!result.ok) {
|
||||
// The SDK needs a rejected execution to emit a tool-error, not a successful result.
|
||||
return Promise.reject(new Error(result.error));
|
||||
}
|
||||
return {
|
||||
content,
|
||||
content: result.data,
|
||||
};
|
||||
},
|
||||
}),
|
||||
pythonTool: tool({
|
||||
description: 'Executes a Python code snippet',
|
||||
});
|
||||
|
||||
const pythonTool = tool({
|
||||
description: 'Executes Python in a sandbox. The value of the last expression is returned automatically (no print needed). stdout from print() is also included.',
|
||||
inputSchema: z.object({
|
||||
code: z.string(),
|
||||
code: z.string().describe('Python code to run. Prefer a final expression over print(), e.g. `2 + 2` returns `4`.'),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
output: z.string(),
|
||||
@@ -549,37 +395,7 @@ const { listDirectoryTool, globTool, readFileTool, readFilesTool, fetchUrlTool,
|
||||
output,
|
||||
};
|
||||
},
|
||||
}),
|
||||
bashTool: tool({
|
||||
description: 'Executes a Bash command',
|
||||
inputSchema: z.object({
|
||||
code: z.string(),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
output: z.string(),
|
||||
}),
|
||||
execute: async ({ code }) => {
|
||||
const { exec } = await import('child_process');
|
||||
const { promisify } = await import('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function runCommand(command: string) {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return stderr;
|
||||
} else {
|
||||
return stdout;
|
||||
}
|
||||
}
|
||||
|
||||
const output = await runCommand(code);
|
||||
return {
|
||||
output,
|
||||
};
|
||||
},
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
async function generateResponse(
|
||||
message: MessageEntity,
|
||||
@@ -588,8 +404,9 @@ async function generateResponse(
|
||||
model: Model,
|
||||
parameters?: Record<string, any>,
|
||||
},
|
||||
search: false | {
|
||||
config: SearchTheWebConfig,
|
||||
search: boolean,
|
||||
enabledTools: {
|
||||
python?: boolean;
|
||||
},
|
||||
generationId: string,
|
||||
userId: string,
|
||||
@@ -608,66 +425,53 @@ async function generateResponse(
|
||||
const activeToolCalls = new Set<string>();
|
||||
const nativeToDbToolCallId = new Map<string, string>();
|
||||
|
||||
// TODO: somehow let the user turn on and off tools
|
||||
const tools: Record<string, Tool> = {
|
||||
// writeFile: tool({
|
||||
// inputSchema: z.object({
|
||||
// path: z.string(),
|
||||
// content: z.string(),
|
||||
// }),
|
||||
// outputSchema: z.object({
|
||||
// success: z.boolean(),
|
||||
// }),
|
||||
// execute: async ({ path, content }) => {
|
||||
// await fs.writeFile(path, content);
|
||||
// return {
|
||||
// success: true,
|
||||
// };
|
||||
// }
|
||||
// }),
|
||||
listDirectory: listDirectoryTool,
|
||||
glob: globTool,
|
||||
readFile: readFileTool,
|
||||
readFiles: readFilesTool,
|
||||
// fetchUrl: fetchUrlTool,
|
||||
python: pythonTool,
|
||||
bash: bashTool,
|
||||
};
|
||||
const tools: Record<string, Tool> = {};
|
||||
|
||||
if (enabledTools.python) {
|
||||
tools.python = pythonTool;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
tools.search = tool({
|
||||
description: 'Searches the web',
|
||||
inputSchema: z.object({
|
||||
query: z.string(),
|
||||
}),
|
||||
outputSchema: z.array(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
link: z.string(),
|
||||
snippet: z.string(),
|
||||
engine: z.string(),
|
||||
})
|
||||
),
|
||||
execute: async ({ query }) => {
|
||||
return await searchTheWeb(search.config)(query);
|
||||
description: `Searches the web. Choose how many results to return on every call using the required results argument.
|
||||
|
||||
CITATION FORMAT:
|
||||
Every result includes a sourceId. Cite supporting results immediately after the claim using this exact construction:
|
||||
"[[cite:" + the result's sourceId + "]]"
|
||||
The prefix "cite:" is mandatory literal text. Copy the sourceId unchanged after that prefix. Do not output the word "sourceId" as a placeholder.
|
||||
|
||||
Example: if a result has sourceId "sd625ebbd4d3544e4914f5a51840d7632":
|
||||
CORRECT: The report describes rising prices. [[cite:sd625ebbd4d3544e4914f5a51840d7632]]
|
||||
INCORRECT: The report describes rising prices. [[sd625ebbd4d3544e4914f5a51840d7632]]
|
||||
The incorrect form is missing "cite:" and will not render as a citation.
|
||||
|
||||
For multiple sources, put their exact IDs after a single "cite:" prefix, separated by commas without spaces: [[cite:ID_FROM_FIRST_RESULT,ID_FROM_SECOND_RESULT]]. Replace both placeholders with returned sourceId values. Separate complete citations are also valid: [[cite:ID_FROM_FIRST_RESULT]] [[cite:ID_FROM_SECOND_RESULT]].
|
||||
Use only IDs returned by searches during this response; never invent IDs or copy the example ID unless it actually appears in a result. Write citation markers directly in answer text, not inside backticks or code blocks. Do not repeat URLs or add footnote definitions for these citations.
|
||||
Search snippets are excerpts, not full-page reads.`,
|
||||
inputSchema: searchInputSchema,
|
||||
outputSchema: searchOutputSchema,
|
||||
execute: async ({ query, results }) => {
|
||||
return await searchTheWeb(query, results);
|
||||
},
|
||||
});
|
||||
tools.fetchUrl = fetchUrlTool;
|
||||
}
|
||||
|
||||
const response = streamText({
|
||||
model: model.gateway(model.model.externalId),
|
||||
messages,
|
||||
allowSystemInMessages: true,
|
||||
providerOptions: {
|
||||
openrouter: {
|
||||
debug: {
|
||||
echo_upstream_body: true,
|
||||
},
|
||||
user: userId,
|
||||
}
|
||||
},
|
||||
},
|
||||
experimental_transform: streamTransoforms,
|
||||
stopWhen: isLoopFinished(),
|
||||
tools: model.model.capabilities.includes('tools') ? tools : undefined,
|
||||
tools: model.model.capabilities.includes('tools') && Object.keys(tools).length > 0 ? tools : undefined,
|
||||
onError: async (error: any) => {
|
||||
// TODO: the docs say "The stream processing will pause until the callback promise is resolved." Suggesting that this error might not be fatal?
|
||||
console.error('generation error', error);
|
||||
@@ -689,6 +493,7 @@ async function generateResponse(
|
||||
|
||||
// Mark failed tool calls
|
||||
await Promise.all([...activeToolCalls].map(async id => {
|
||||
const lastUpdatedAt = new Date();
|
||||
await db.update(toolCalls).set({
|
||||
status: 'failed',
|
||||
error: { type: ToolCallType.Text, value: 'Generation failed' }
|
||||
@@ -700,10 +505,12 @@ async function generateResponse(
|
||||
messageId: message.id,
|
||||
toolCallId: id,
|
||||
toolName: null,
|
||||
status: 'failed',
|
||||
error: {
|
||||
type: ToolCallType.Text,
|
||||
value: 'Generation failed'
|
||||
}
|
||||
},
|
||||
lastUpdatedAt: lastUpdatedAt.toISOString(),
|
||||
}
|
||||
})
|
||||
}));
|
||||
@@ -859,6 +666,7 @@ async function generateResponse(
|
||||
partId: part.id,
|
||||
lastUpdatedAt: new Date(),
|
||||
content: token.text,
|
||||
offset: part.accumulatedContent.length - token.text.length,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -959,7 +767,7 @@ async function generateResponse(
|
||||
throw new Error('Failed to insert message part');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-ignore - doesnt exist on the type but yeah it does now
|
||||
part.toolCall = toolCall;
|
||||
|
||||
await topicEvents.emit(topicId, {
|
||||
@@ -972,8 +780,6 @@ async function generateResponse(
|
||||
})
|
||||
|
||||
activeToolCalls.add(dbToolCallId);
|
||||
|
||||
activeParts.set(key, { id: part.id, accumulatedContent: '' });
|
||||
} break;
|
||||
case 'tool-call': {
|
||||
let inputType: ToolCallType = ToolCallType.Text;
|
||||
@@ -1005,6 +811,11 @@ async function generateResponse(
|
||||
},
|
||||
})
|
||||
.where(eq(toolCalls.id, dbToolCallId));
|
||||
if (token.providerMetadata) await db.update(messageParts)
|
||||
.set({
|
||||
providerOptions: token.providerMetadata,
|
||||
})
|
||||
.where(eq(messageParts.toolCallId, dbToolCallId));
|
||||
|
||||
await topicEvents.emit(topicId, {
|
||||
type: 'tool-call-delta',
|
||||
@@ -1049,6 +860,7 @@ async function generateResponse(
|
||||
topicId,
|
||||
messageId: message.id,
|
||||
toolCallId: dbToolCallId,
|
||||
providerOptions: token.providerMetadata,
|
||||
type: 'tool-call',
|
||||
content: null,
|
||||
finished: false,
|
||||
@@ -1060,7 +872,7 @@ async function generateResponse(
|
||||
throw new Error('Failed to insert message part');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-ignore - doesnt exist on the type but yeah it does now
|
||||
part.toolCall = toolCall;
|
||||
|
||||
await topicEvents.emit(topicId, {
|
||||
@@ -1072,8 +884,6 @@ async function generateResponse(
|
||||
})
|
||||
|
||||
activeToolCalls.add(dbToolCallId);
|
||||
|
||||
activeParts.set(key, { id: part.id, accumulatedContent: '' });
|
||||
}
|
||||
} break;
|
||||
case 'tool-result': {
|
||||
@@ -1097,7 +907,13 @@ async function generateResponse(
|
||||
status: 'failed',
|
||||
error: { type: ToolCallType.Text, value: 'Tool returned invalid output' }
|
||||
}).where(eq(toolCalls.id, dbToolCallId));
|
||||
if (token.providerMetadata) await db.update(messageParts)
|
||||
.set({
|
||||
providerOptions: token.providerMetadata,
|
||||
})
|
||||
.where(eq(messageParts.toolCallId, dbToolCallId));
|
||||
|
||||
const lastUpdatedAt = new Date();
|
||||
await topicEvents.emit(topicId, {
|
||||
type: 'tool-call-delta',
|
||||
payload: {
|
||||
@@ -1109,6 +925,7 @@ async function generateResponse(
|
||||
value: outputValue,
|
||||
},
|
||||
status: 'failed',
|
||||
lastUpdatedAt: lastUpdatedAt.toISOString(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1118,6 +935,7 @@ async function generateResponse(
|
||||
}
|
||||
|
||||
if (dbToolCallId) {
|
||||
const lastUpdatedAt = new Date();
|
||||
await db.update(toolCalls).set({
|
||||
status: 'completed',
|
||||
output: {
|
||||
@@ -1125,6 +943,13 @@ async function generateResponse(
|
||||
value: outputValue,
|
||||
},
|
||||
}).where(eq(toolCalls.id, dbToolCallId));
|
||||
await db.update(messageParts)
|
||||
.set({
|
||||
finished: true,
|
||||
lastUpdatedAt,
|
||||
...(token.providerMetadata ? { providerOptions: token.providerMetadata } : {}),
|
||||
})
|
||||
.where(eq(messageParts.toolCallId, dbToolCallId));
|
||||
|
||||
await topicEvents.emit(topicId, {
|
||||
type: 'tool-call-delta',
|
||||
@@ -1137,6 +962,7 @@ async function generateResponse(
|
||||
value: outputValue,
|
||||
},
|
||||
status: 'completed',
|
||||
lastUpdatedAt: lastUpdatedAt.toISOString(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1157,8 +983,8 @@ async function generateResponse(
|
||||
outputValue = token.error;
|
||||
break;
|
||||
case 'object':
|
||||
outputType = ToolCallType.Json;
|
||||
outputValue = JSON.stringify(token.error);
|
||||
outputType = token.error instanceof Error ? ToolCallType.Text : ToolCallType.Json;
|
||||
outputValue = serializeToolError(token.error);
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown error type', token.error);
|
||||
@@ -1169,6 +995,7 @@ async function generateResponse(
|
||||
|
||||
const existingDbToolCallId = nativeToDbToolCallId.get(token.toolCallId);
|
||||
if (existingDbToolCallId && activeToolCalls.has(existingDbToolCallId)) {
|
||||
const lastUpdatedAt = new Date();
|
||||
await db.update(toolCalls).set({
|
||||
status: 'failed',
|
||||
error: {
|
||||
@@ -1176,6 +1003,13 @@ async function generateResponse(
|
||||
value: outputValue as string,
|
||||
}
|
||||
}).where(eq(toolCalls.id, existingDbToolCallId));
|
||||
await db.update(messageParts)
|
||||
.set({
|
||||
finished: true,
|
||||
lastUpdatedAt,
|
||||
...(token.providerMetadata ? { providerOptions: token.providerMetadata } : {}),
|
||||
})
|
||||
.where(eq(messageParts.toolCallId, existingDbToolCallId));
|
||||
|
||||
await topicEvents.emit(topicId, {
|
||||
type: 'tool-call-delta',
|
||||
@@ -1183,10 +1017,12 @@ async function generateResponse(
|
||||
messageId: message.id,
|
||||
toolCallId: existingDbToolCallId,
|
||||
toolName: token.toolName,
|
||||
status: 'failed',
|
||||
error: {
|
||||
type: outputType as ToolCallType,
|
||||
value: outputValue as string,
|
||||
},
|
||||
lastUpdatedAt: lastUpdatedAt.toISOString(),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -1220,6 +1056,7 @@ async function generateResponse(
|
||||
topicId,
|
||||
messageId: message.id,
|
||||
toolCallId: dbToolCallId,
|
||||
providerOptions: token.providerMetadata,
|
||||
type: 'tool-call',
|
||||
content: null,
|
||||
finished: false,
|
||||
@@ -1235,11 +1072,9 @@ async function generateResponse(
|
||||
type: 'tool-call-start',
|
||||
payload: {
|
||||
messageId: message.id,
|
||||
part,
|
||||
part: { ...part, toolCall },
|
||||
}
|
||||
})
|
||||
|
||||
activeParts.set(key, { id: part.id, accumulatedContent: '' });
|
||||
}
|
||||
|
||||
if (existingDbToolCallId) {
|
||||
@@ -1250,12 +1085,8 @@ async function generateResponse(
|
||||
|
||||
case 'finish': {
|
||||
let tps;
|
||||
if (ttft !== undefined && token.totalUsage.outputTokens !== undefined) {
|
||||
const tokenStreamStart = requestStart! + ttft;
|
||||
// this is the *real* request duration, excluding the
|
||||
// TTFT
|
||||
const requestDuration = performance.now() - tokenStreamStart;
|
||||
|
||||
if (requestStart !== undefined && token.totalUsage.outputTokens !== undefined) {
|
||||
const requestDuration = performance.now() - requestStart;
|
||||
tps = token.totalUsage.outputTokens / (requestDuration / 1000);
|
||||
}
|
||||
|
||||
@@ -1428,6 +1259,7 @@ async function generateResponse(
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
topicEvents.clearLiveText(topicId, message.id);
|
||||
completeGeneration(generationId);
|
||||
if (logFile !== undefined) logFile.close();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import * as z from 'zod';
|
||||
import { messages, messageParts, attachments, topics, generations, toolCalls } from '~~/drizzle/schema';
|
||||
import { db } from '~~/server/lib/db';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { eq, and, inArray } from 'drizzle-orm';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
|
||||
const userId = event.context.user!.id;
|
||||
const topicId = getRouterParam(event, 'topicId')!;
|
||||
|
||||
const result = await readValidatedBody(event, (body) =>
|
||||
z
|
||||
.object({
|
||||
messageId: z.string(),
|
||||
})
|
||||
.safeParse(body),
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: result.error.issues[0]!.message,
|
||||
});
|
||||
}
|
||||
|
||||
const { messageId } = result.data;
|
||||
|
||||
const topic = await db.select().from(topics).where(and(
|
||||
eq(topics.id, topicId),
|
||||
eq(topics.userId, userId),
|
||||
)).limit(1).then(rows => rows[0]);
|
||||
|
||||
if (!topic) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Not Found',
|
||||
message: 'Topic not found',
|
||||
});
|
||||
}
|
||||
|
||||
const topicMessages = await db.select().from(messages)
|
||||
.where(eq(messages.topicId, topicId))
|
||||
.orderBy(messages.createdAt);
|
||||
|
||||
const forkMessage = topicMessages.find(m => m.id === messageId);
|
||||
if (!forkMessage) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Not Found',
|
||||
message: 'Message not found in topic',
|
||||
});
|
||||
}
|
||||
|
||||
const childrenMap = new Map<string, typeof topicMessages>();
|
||||
for (const msg of topicMessages) {
|
||||
if (msg.parentMessageId) {
|
||||
const siblings = childrenMap.get(msg.parentMessageId) || [];
|
||||
siblings.push(msg);
|
||||
childrenMap.set(msg.parentMessageId, siblings);
|
||||
}
|
||||
}
|
||||
|
||||
const messageIdsToCopy = new Set<string>();
|
||||
const messageMap = new Map(topicMessages.map(m => [m.id, m]));
|
||||
|
||||
const addMessageAndSiblings = (msgId: string) => {
|
||||
const msg = messageMap.get(msgId);
|
||||
if (!msg) return;
|
||||
|
||||
if (msg.parentMessageId) {
|
||||
const siblings = childrenMap.get(msg.parentMessageId) || [];
|
||||
for (const sibling of siblings) {
|
||||
messageIdsToCopy.add(sibling.id);
|
||||
}
|
||||
} else {
|
||||
messageIdsToCopy.add(msgId);
|
||||
}
|
||||
};
|
||||
|
||||
let current: typeof forkMessage | undefined = forkMessage;
|
||||
while (current) {
|
||||
addMessageAndSiblings(current.id);
|
||||
current = current.parentMessageId ? messageMap.get(current.parentMessageId) : undefined;
|
||||
}
|
||||
|
||||
const forkIndex = topicMessages.findIndex(m => m.id === messageId);
|
||||
for (let i = 0; i <= forkIndex; i++) {
|
||||
const msg = topicMessages[i]!;
|
||||
if (!msg.parentMessageId) {
|
||||
messageIdsToCopy.add(msg.id);
|
||||
const children = childrenMap.get(msg.id) || [];
|
||||
for (const child of children) {
|
||||
messageIdsToCopy.add(child.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addDescendants = (msgId: string) => {
|
||||
const children = childrenMap.get(msgId) || [];
|
||||
for (const child of children) {
|
||||
if (!messageIdsToCopy.has(child.id)) {
|
||||
messageIdsToCopy.add(child.id);
|
||||
addDescendants(child.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initialIds = Array.from(messageIdsToCopy);
|
||||
for (const id of initialIds) {
|
||||
addDescendants(id);
|
||||
}
|
||||
|
||||
const messagesToCopy = topicMessages
|
||||
.filter(m => messageIdsToCopy.has(m.id))
|
||||
.sort((a, b) => {
|
||||
if (!a.parentMessageId && b.parentMessageId) return -1;
|
||||
if (a.parentMessageId && !b.parentMessageId) return 1;
|
||||
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
});
|
||||
|
||||
const newTopicId = nanoid();
|
||||
const newTopicName = `${topic.name} (fork)`;
|
||||
|
||||
await db.insert(topics).values({
|
||||
id: newTopicId,
|
||||
userId,
|
||||
name: newTopicName,
|
||||
agentId: topic.agentId,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
const messageIdMap = new Map<string, string>();
|
||||
const generationIdMap = new Map<string, string>();
|
||||
const toolCallIdMap = new Map<string, string>();
|
||||
|
||||
const originalMessageIds = messagesToCopy.map(m => m.id);
|
||||
const originalGenerationIds = [
|
||||
...new Set(
|
||||
messagesToCopy
|
||||
.map(m => m.generationId)
|
||||
.filter((id): id is string => id !== null),
|
||||
),
|
||||
];
|
||||
|
||||
// Copy generations first so messages can reference them
|
||||
if (originalGenerationIds.length > 0) {
|
||||
const generationsToCopy = await db.select().from(generations)
|
||||
.where(inArray(generations.id, originalGenerationIds));
|
||||
|
||||
for (const generation of generationsToCopy) {
|
||||
const newGenerationId = nanoid();
|
||||
generationIdMap.set(generation.id, newGenerationId);
|
||||
|
||||
await db.insert(generations).values({
|
||||
id: newGenerationId,
|
||||
userId,
|
||||
topicId: newTopicId,
|
||||
modelId: generation.modelId,
|
||||
status: generation.status,
|
||||
tokens: generation.tokens,
|
||||
error: generation.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const msg of messagesToCopy) {
|
||||
const newMessageId = nanoid();
|
||||
messageIdMap.set(msg.id, newMessageId);
|
||||
|
||||
await db.insert(messages).values({
|
||||
id: newMessageId,
|
||||
userId,
|
||||
topicId: newTopicId,
|
||||
parentMessageId: msg.parentMessageId ? (messageIdMap.get(msg.parentMessageId) ?? null) : null,
|
||||
generationId: msg.generationId ? (generationIdMap.get(msg.generationId) ?? null) : null,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
activeChildId: null,
|
||||
deleted: msg.deleted,
|
||||
createdAt: msg.createdAt,
|
||||
updatedAt: msg.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
for (const msg of messagesToCopy) {
|
||||
if (msg.activeChildId) {
|
||||
const newMessageId = messageIdMap.get(msg.id);
|
||||
const newActiveChildId = messageIdMap.get(msg.activeChildId);
|
||||
if (newMessageId && newActiveChildId) {
|
||||
await db.update(messages)
|
||||
.set({ activeChildId: newActiveChildId })
|
||||
.where(eq(messages.id, newMessageId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (originalMessageIds.length > 0) {
|
||||
const parts = await db.select().from(messageParts)
|
||||
.where(inArray(messageParts.messageId, originalMessageIds));
|
||||
|
||||
// Copy referenced tool calls before parts so FKs resolve
|
||||
const originalToolCallIds = [
|
||||
...new Set(
|
||||
parts
|
||||
.map(p => p.toolCallId)
|
||||
.filter((id): id is string => id !== null),
|
||||
),
|
||||
];
|
||||
|
||||
if (originalToolCallIds.length > 0) {
|
||||
const toolCallsToCopy = await db.select().from(toolCalls)
|
||||
.where(inArray(toolCalls.id, originalToolCallIds));
|
||||
|
||||
for (const toolCall of toolCallsToCopy) {
|
||||
const newToolCallId = nanoid();
|
||||
toolCallIdMap.set(toolCall.id, newToolCallId);
|
||||
|
||||
await db.insert(toolCalls).values({
|
||||
id: newToolCallId,
|
||||
userId,
|
||||
toolName: toolCall.toolName,
|
||||
status: toolCall.status,
|
||||
input: toolCall.input,
|
||||
output: toolCall.output,
|
||||
error: toolCall.error,
|
||||
createdAt: toolCall.createdAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
const newMessageId = messageIdMap.get(part.messageId);
|
||||
if (!newMessageId) continue;
|
||||
|
||||
await db.insert(messageParts).values({
|
||||
userId,
|
||||
topicId: newTopicId,
|
||||
messageId: newMessageId,
|
||||
toolCallId: part.toolCallId ? (toolCallIdMap.get(part.toolCallId) ?? null) : null,
|
||||
type: part.type,
|
||||
content: part.content,
|
||||
providerOptions: part.providerOptions,
|
||||
finished: part.finished,
|
||||
// Preserve original timestamps so reasoning duration stays accurate
|
||||
createdAt: part.createdAt,
|
||||
lastUpdatedAt: part.lastUpdatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
const attachmentsResult = await db.select().from(attachments)
|
||||
.where(inArray(attachments.messageId, originalMessageIds));
|
||||
|
||||
for (const attachment of attachmentsResult) {
|
||||
const newMessageId = messageIdMap.get(attachment.messageId);
|
||||
if (!newMessageId) continue;
|
||||
|
||||
await db.insert(attachments).values({
|
||||
userId,
|
||||
topicId: newTopicId,
|
||||
messageId: newMessageId,
|
||||
fileId: attachment.fileId,
|
||||
createdAt: attachment.createdAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
topicId: newTopicId,
|
||||
name: newTopicName,
|
||||
};
|
||||
});
|
||||
@@ -1,95 +1,60 @@
|
||||
import { db } from "~~/server/lib/db";
|
||||
import { topicEvents } from "~~/server/utils/events";
|
||||
import { db } from '~~/server/lib/db';
|
||||
import { topicEvents } from '~~/server/utils/events';
|
||||
import { createTopicSnapshotStream } from '~~/server/utils/topic-snapshot-stream';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
|
||||
const topicId = getRouterParam(event, 'topicId')!;
|
||||
const isSSE = getHeader(event, 'accept')?.includes('text/event-stream') ?? false;
|
||||
|
||||
const userId = event.context.user!.id as string;
|
||||
|
||||
const topic = await db.query.topics.findFirst({
|
||||
where: {
|
||||
id: topicId,
|
||||
userId,
|
||||
},
|
||||
const isSSE = getHeader(event, 'accept')?.includes('text/event-stream') ?? false;
|
||||
const readTopic = () => db.query.topics.findFirst({
|
||||
where: { id: topicId, userId },
|
||||
with: {
|
||||
messages: {
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
with: {
|
||||
parts: {
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
with: {
|
||||
toolCall: true,
|
||||
}
|
||||
},
|
||||
attachments: {
|
||||
with: {
|
||||
file: true,
|
||||
}
|
||||
orderBy: { createdAt: 'asc' },
|
||||
with: { toolCall: true },
|
||||
},
|
||||
attachments: { with: { file: true } },
|
||||
generation: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!isSSE) {
|
||||
const topic = await readTopic();
|
||||
if (!topic) {
|
||||
console.log("topic not found");
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Not Found',
|
||||
});
|
||||
setResponseStatus(event, 404);
|
||||
return { ok: false, error: 'Topic not found' };
|
||||
}
|
||||
|
||||
if (isSSE === false) {
|
||||
topicEvents.applyLiveText(topicId, topic.messages);
|
||||
return topic;
|
||||
}
|
||||
|
||||
// Authorize before subscribing; no topic data is exposed before this check.
|
||||
const ownedTopic = await db.query.topics.findFirst({
|
||||
where: { id: topicId, userId },
|
||||
columns: { id: true },
|
||||
});
|
||||
if (!ownedTopic) {
|
||||
setResponseStatus(event, 404);
|
||||
return { ok: false, error: 'Topic not found' };
|
||||
}
|
||||
|
||||
const subscription = createTopicSnapshotStream(topicId, getQuery(event).snapshot);
|
||||
event.node.res.once('close', subscription.close);
|
||||
const stream = await subscription.open(readTopic);
|
||||
if (!stream.ok) {
|
||||
setResponseStatus(event, stream.error === 'read-failed' ? 500 : 404);
|
||||
return { ok: false, error: stream.error };
|
||||
}
|
||||
setHeader(event, 'Content-Type', 'text/event-stream');
|
||||
setHeader(event, 'Cache-Control', 'no-cache');
|
||||
setHeader(event, 'Connection', 'keep-alive');
|
||||
setHeader(event, 'X-Accel-Buffering', 'no');
|
||||
|
||||
const { lastUpdate, count } = getQuery(event);
|
||||
const serverLastUpdate = topic.messages.at(-1)?.updatedAt;
|
||||
const serverCount = topic.messages.length;
|
||||
|
||||
// TODO: there is potentially a race condition here where the client could
|
||||
// connect at milisecond 87, and the last database write was at 0 (there
|
||||
// is a write every 100ms for some operations), if there were tokens sent
|
||||
// at 20 40 and 60, those tokens are lost to the client.
|
||||
let streamController: ReadableStreamDefaultController;
|
||||
let interval: NodeJS.Timeout;
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
streamController = controller;
|
||||
controller.enqueue(`:connected\n\n`);
|
||||
|
||||
if ((lastUpdate && count) && (lastUpdate === serverLastUpdate?.toISOString() && count === serverCount)) {
|
||||
controller.enqueue(':already_synced\n\n')
|
||||
} else {
|
||||
controller.enqueue(`data: ${JSON.stringify({
|
||||
type: 'initial_state',
|
||||
payload: topic
|
||||
})}\n\n`);
|
||||
}
|
||||
|
||||
interval = setInterval(() => {
|
||||
controller.enqueue(`:heartbeat\n\n`);
|
||||
}, 15000);
|
||||
|
||||
topicEvents.subscribe(topicId, streamController);
|
||||
},
|
||||
cancel() {
|
||||
topicEvents.unsubscribe(topicId, streamController);
|
||||
clearInterval(interval);
|
||||
}
|
||||
});
|
||||
|
||||
return sendStream(event, stream);
|
||||
return sendStream(event, stream.data);
|
||||
});
|
||||
|
||||
@@ -29,6 +29,12 @@ export default defineEventHandler(async (event) => {
|
||||
const res = await db.update(topics)
|
||||
.set({
|
||||
name,
|
||||
...(name === undefined ? {} : {
|
||||
titleSource: "manual",
|
||||
renaming: false,
|
||||
renameAttemptId: null,
|
||||
renameExpiresAt: null,
|
||||
}),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { modelMessageSchema } from 'ai';
|
||||
import * as z from 'zod';
|
||||
import { attachments, messages } from '~~/drizzle/schema';
|
||||
import { attachments, messageParts, messages } from '~~/drizzle/schema';
|
||||
import { db } from '~~/server/lib/db';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -9,6 +9,21 @@ export default defineEventHandler(async (event) => {
|
||||
const topicId = getRouterParam(event, 'topicId')!;
|
||||
const userId = event.context.user!.id;
|
||||
|
||||
const topic = await db.query.topics.findFirst({
|
||||
where: {
|
||||
id: topicId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!topic) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Not Found',
|
||||
message: 'Topic not found',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await readValidatedBody(event, z.object({
|
||||
message: z.intersection(
|
||||
z.object({
|
||||
@@ -19,6 +34,7 @@ export default defineEventHandler(async (event) => {
|
||||
),
|
||||
}).safeParse);
|
||||
if (!result.success) {
|
||||
console.log(result.error);
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Bad Request',
|
||||
@@ -28,6 +44,16 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const { message } = result.data;
|
||||
|
||||
if (['user', 'assistant'].includes(message.role) === false) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Bad Request',
|
||||
message: 'Invalid role',
|
||||
});
|
||||
}
|
||||
|
||||
switch (message.role) {
|
||||
case 'user': {
|
||||
await db.insert(messages).values({
|
||||
// @ts-ignore - drizzle bug
|
||||
id: message.id,
|
||||
@@ -73,4 +99,55 @@ export default defineEventHandler(async (event) => {
|
||||
return {
|
||||
ok: true
|
||||
};
|
||||
}
|
||||
case 'assistant': {
|
||||
const [dbmessage] = await db.insert(messages).values({
|
||||
// @ts-ignore - drizzle bug
|
||||
id: message.id,
|
||||
userId,
|
||||
topicId,
|
||||
parentMessageId: null,
|
||||
generationId: null,
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
}).returning();
|
||||
|
||||
if (!dbmessage) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to insert message',
|
||||
message: 'Failed to insert message',
|
||||
});
|
||||
}
|
||||
|
||||
const [part] = await db.insert(messageParts).values({
|
||||
userId,
|
||||
topicId,
|
||||
messageId: dbmessage.id,
|
||||
type: 'text',
|
||||
content: message.content,
|
||||
providerOptions: null,
|
||||
finished: true,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
}).returning();
|
||||
|
||||
// TODO: transaction
|
||||
if (!part) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to insert message part',
|
||||
message: 'Failed to insert message part',
|
||||
});
|
||||
}
|
||||
|
||||
topicEvents.emit(topicId, { type: 'MESSAGE_CREATED', payload: dbmessage });
|
||||
topicEvents.emit(topicId, { type: 'text-start', payload: { messageId: dbmessage.id, part } });
|
||||
topicEvents.emit(topicId, { type: 'text-end', payload: { messageId: dbmessage.id, partId: part.id, lastUpdatedAt: new Date(), content: message.content } });
|
||||
|
||||
return {
|
||||
ok: true
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -30,6 +30,7 @@ export default defineEventHandler(async (event) => {
|
||||
id,
|
||||
userId,
|
||||
name,
|
||||
titleSource: "provisional",
|
||||
agentId,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
@@ -39,7 +39,22 @@ export default defineEventHandler(async (event) => {
|
||||
responseChecksumValidation: 'WHEN_REQUIRED',
|
||||
});
|
||||
|
||||
const key = `veridian__uploads/${Date.now()}-${file.name.replace(/[^a-zA-Z0-9.]/g, '_')}-${event.context.user!.id}`
|
||||
const fileParts = file.name.split('.');
|
||||
if (fileParts.length < 2) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid file name',
|
||||
data: {
|
||||
code: 'INVALID_FILE_NAME',
|
||||
ok: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fileExt = fileParts.pop()!;
|
||||
const fileName = fileParts.join('.').replace(/[^a-zA-Z0-9.]/g, '_');
|
||||
|
||||
const key = `veridian__uploads/${Date.now()}-${fileName}-${event.context.user!.id}.${fileExt}`;
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
ACL: 'public-read',
|
||||
@@ -56,7 +71,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
return {
|
||||
url,
|
||||
assetUrl: `${process.env.NUXT_PUBLIC_URL}/api/files/${key}`,
|
||||
assetUrl: `/api/files/${key}`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to generate presigned URL:', error);
|
||||
|
||||
@@ -67,11 +67,3 @@ export async function getProviderDetails(provider: ProviderDrizzle, providerApiK
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function isRerankingProvider(gateway: any): gateway is { reranking: (modelId: string) => any } {
|
||||
return (
|
||||
gateway !== null &&
|
||||
'reranking' in gateway &&
|
||||
typeof (gateway as any).reranking === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
+14
-6
@@ -1,5 +1,9 @@
|
||||
import { createLiveTopicText } from './live-topic-text';
|
||||
|
||||
type StreamSink = Pick<ReadableStreamDefaultController, 'enqueue'>;
|
||||
|
||||
interface StreamConnection {
|
||||
controller: ReadableStreamDefaultController;
|
||||
controller: StreamSink;
|
||||
lastPing: number;
|
||||
subscribedAt: number;
|
||||
}
|
||||
@@ -91,9 +95,13 @@ export const userEvents = {
|
||||
};
|
||||
|
||||
const topicSubscribers = new Map<string, Set<StreamConnection>>();
|
||||
const liveTopicText = createLiveTopicText();
|
||||
|
||||
export const topicEvents = {
|
||||
subscribe(topicId: string, controller: ReadableStreamDefaultController) {
|
||||
applyLiveText: liveTopicText.apply,
|
||||
clearLiveText: liveTopicText.removeMessage,
|
||||
|
||||
subscribe(topicId: string, controller: StreamSink) {
|
||||
if (!topicSubscribers.has(topicId)) {
|
||||
topicSubscribers.set(topicId, new Set());
|
||||
}
|
||||
@@ -120,7 +128,7 @@ export const topicEvents = {
|
||||
(conn as any).pingInterval = pingInterval;
|
||||
},
|
||||
|
||||
unsubscribe(topicId: string, controller: ReadableStreamDefaultController) {
|
||||
unsubscribe(topicId: string, controller: StreamSink) {
|
||||
const connections = topicSubscribers.get(topicId);
|
||||
if (!connections) return;
|
||||
|
||||
@@ -137,14 +145,14 @@ export const topicEvents = {
|
||||
},
|
||||
|
||||
async emit(topicId: string, events: Array<{ type: string; payload: any; timestamp?: number }> | { type: string; payload: any; timestamp?: number }) {
|
||||
const connections = topicSubscribers.get(topicId);
|
||||
if (!connections || connections.size === 0) return;
|
||||
|
||||
if (!Array.isArray(events)) {
|
||||
events = [events];
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
liveTopicText.update(topicId, event);
|
||||
const connections = topicSubscribers.get(topicId);
|
||||
if (!connections || connections.size === 0) continue;
|
||||
const timestamp = event.timestamp ?? Date.now();
|
||||
const data = `data: ${JSON.stringify({ ...event, timestamp })}\n\n`;
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const DEFAULT_EXPIRY_MS = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
export function generateFileToken(
|
||||
fileKey: string,
|
||||
secret: string,
|
||||
expiresInMs = DEFAULT_EXPIRY_MS,
|
||||
): { exp: number; sig: string } {
|
||||
const exp = Date.now() + expiresInMs;
|
||||
const payload = `${fileKey}:${exp}`;
|
||||
const sig = createHmac('sha256', secret).update(payload).digest('hex');
|
||||
return { exp, sig };
|
||||
}
|
||||
|
||||
export function verifyFileToken(
|
||||
fileKey: string,
|
||||
exp: number,
|
||||
sig: string,
|
||||
secret: string,
|
||||
): boolean {
|
||||
if (Date.now() > exp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const payload = `${fileKey}:${exp}`;
|
||||
const expected = createHmac('sha256', secret).update(payload).digest('hex');
|
||||
|
||||
const sigBuffer = Buffer.from(sig, 'hex');
|
||||
const expectedBuffer = Buffer.from(expected, 'hex');
|
||||
|
||||
if (sigBuffer.length !== expectedBuffer.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return timingSafeEqual(sigBuffer, expectedBuffer);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Result } from "../../types/result";
|
||||
import { z } from "zod";
|
||||
import { Err, Ok } from "../../types/result";
|
||||
|
||||
const scrapeResponseSchema = z.object({
|
||||
success: z.boolean().optional(),
|
||||
error: z.union([z.string(), z.object({ message: z.string() })]).optional(),
|
||||
message: z.string().optional(),
|
||||
data: z.unknown().optional(),
|
||||
});
|
||||
|
||||
export const readFirecrawlResponse = async (response: Response): Promise<Result<string, string>> => {
|
||||
const context = `Firecrawl scrape (HTTP ${response.status})`;
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {
|
||||
return Err(`${context}: received a non-JSON or unreadable response.`);
|
||||
}
|
||||
|
||||
const parsed = scrapeResponseSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return Err(`${context}: received an unexpected response format.`);
|
||||
}
|
||||
|
||||
const payload = parsed.data;
|
||||
const providerError = typeof payload.error === "string" ? payload.error : payload.error?.message;
|
||||
if (!response.ok || payload.success === false || providerError) {
|
||||
return Err(`${context}: ${providerError || payload.message || response.statusText || "Scrape failed without an error message."}`);
|
||||
}
|
||||
const content = z.object({ markdown: z.string() }).safeParse(payload.data);
|
||||
if (!content.success || content.data.markdown.trim() === "") {
|
||||
return Err(`${context}: no Markdown content was returned for this URL.`);
|
||||
}
|
||||
return Ok(content.data.markdown);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { appendTextDelta } from '../../utils/topic-sync';
|
||||
|
||||
interface LivePart {
|
||||
messageId: string;
|
||||
part: Record<string, any> & { id: string; content: string };
|
||||
}
|
||||
|
||||
/** Covers the interval between an emitted delta and its throttled DB write. */
|
||||
export const createLiveTopicText = () => {
|
||||
const topics = new Map<string, Map<string, LivePart>>();
|
||||
|
||||
const removeMessage = (topicId: string, messageId: string) => {
|
||||
const parts = topics.get(topicId);
|
||||
if (!parts) return;
|
||||
for (const [id, entry] of parts) {
|
||||
if (entry.messageId === messageId) parts.delete(id);
|
||||
}
|
||||
if (parts.size === 0) topics.delete(topicId);
|
||||
};
|
||||
|
||||
return {
|
||||
update(topicId: string, event: { type: string; payload: any }) {
|
||||
const { type, payload } = event;
|
||||
if (type === 'text-start') {
|
||||
let parts = topics.get(topicId);
|
||||
if (!parts) topics.set(topicId, parts = new Map());
|
||||
parts.set(payload.part.id, {
|
||||
messageId: payload.messageId,
|
||||
part: { ...payload.part, toolCall: null, content: payload.part.content ?? '' },
|
||||
});
|
||||
} else if (type === 'text-delta') {
|
||||
const entry = topics.get(topicId)?.get(payload.partId);
|
||||
if (entry) {
|
||||
entry.part.content = appendTextDelta(entry.part.content, payload.content, payload.offset);
|
||||
entry.part.lastUpdatedAt = payload.lastUpdatedAt;
|
||||
}
|
||||
} else if (type === 'text-end' || type === 'text-delete') {
|
||||
const parts = topics.get(topicId);
|
||||
parts?.delete(payload.partId);
|
||||
if (parts?.size === 0) topics.delete(topicId);
|
||||
} else if (type === 'MESSAGE_DELETED') {
|
||||
removeMessage(topicId, payload.id);
|
||||
}
|
||||
},
|
||||
apply(topicId: string, messages: { id: string; parts: any[] }[]) {
|
||||
const parts = topics.get(topicId);
|
||||
if (!parts) return;
|
||||
const byId = new Map(messages.map(message => [message.id, message]));
|
||||
for (const entry of parts.values()) {
|
||||
const message = byId.get(entry.messageId);
|
||||
if (!message) continue;
|
||||
const existing = message.parts.find(part => part.id === entry.part.id);
|
||||
if (existing) {
|
||||
if (!existing.finished && (existing.content?.length ?? 0) <= entry.part.content.length) {
|
||||
existing.content = entry.part.content;
|
||||
existing.lastUpdatedAt = entry.part.lastUpdatedAt;
|
||||
}
|
||||
} else {
|
||||
message.parts.push({ ...entry.part });
|
||||
}
|
||||
}
|
||||
},
|
||||
removeMessage,
|
||||
};
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
import { type ToolSet, type TextStreamPart, type ToolCallPart } from 'ai';
|
||||
|
||||
export function createLongcatTransformer<TOOLS extends ToolSet>(): (options: {
|
||||
tools: TOOLS;
|
||||
stopStream: () => void;
|
||||
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>> {
|
||||
let buffer = '';
|
||||
let hasToolCallInStep = false;
|
||||
let lastChunkId: string | undefined;
|
||||
let lastChunkType: 'text' | 'reasoning' | undefined;
|
||||
let step = 0;
|
||||
|
||||
return (_opts) => {
|
||||
return new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({
|
||||
transform(chunk, controller) {
|
||||
if (chunk.type === 'finish-step' || chunk.type === 'finish') {
|
||||
step++;
|
||||
|
||||
if (hasToolCallInStep) {
|
||||
// We clone the chunk and overwrite the finishReason.
|
||||
// This tricks the SDK into thinking the model requested a tool natively.
|
||||
const modifiedChunk = {
|
||||
...chunk,
|
||||
finishReason: 'tool-calls' as const,
|
||||
};
|
||||
|
||||
// Reset for the next potential step
|
||||
if (chunk.type === 'finish-step') {
|
||||
hasToolCallInStep = false;
|
||||
}
|
||||
|
||||
controller.enqueue(modifiedChunk);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === 'text-start' || chunk.type === 'reasoning-start') {
|
||||
lastChunkId = chunk.id;
|
||||
lastChunkType = chunk.type.split('-')[1] as 'text' | 'reasoning';
|
||||
}
|
||||
|
||||
// We only care about text chunks
|
||||
if (chunk.type !== 'text-delta' && chunk.type !== 'reasoning-delta') {
|
||||
controller.enqueue(chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
buffer += chunk.text;
|
||||
|
||||
// Check if we have a full tool call in the buffer
|
||||
const pattern = /<longcat_tool_call>([\s\S]*?)<\/longcat_tool_call>/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(buffer)) !== null) {
|
||||
console.log("longcat tool call found at index", match.index);
|
||||
|
||||
// 1. Enqueue any text that appeared BEFORE the tool call
|
||||
const textBefore = buffer.substring(lastIndex, match.index);
|
||||
if (textBefore) {
|
||||
controller.enqueue({ type: chunk.type, text: textBefore, id: lastChunkId ?? chunk.type.includes('reasoning') ? `reasoning-${step}` : `text-${step}` });
|
||||
}
|
||||
|
||||
// 2. Parse the XML content
|
||||
const content = match[1]!.trim();
|
||||
const toolNameMatch = content.match(/^([^\s<]+)/);
|
||||
|
||||
if (toolNameMatch) {
|
||||
hasToolCallInStep = true;
|
||||
|
||||
const toolName = toolNameMatch[1];
|
||||
const args: Record<string, any> = {};
|
||||
const argRegex = /<longcat_arg_key>(.*?)<\/longcat_arg_key>\s*<longcat_arg_value>(.*?)<\/longcat_arg_value>/gs;
|
||||
|
||||
let argMatch;
|
||||
while ((argMatch = argRegex.exec(content)) !== null) {
|
||||
args[argMatch[1]!.trim()] = argMatch[2]!.trim();
|
||||
}
|
||||
|
||||
// 3. EMIT A TOOL CALL PART
|
||||
// This is the "magic" - the SDK will see this and act as if the LLM
|
||||
// called a native tool.
|
||||
const toolCallId = `lc-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||
controller.enqueue({
|
||||
type: 'tool-call',
|
||||
// @ts-ignore
|
||||
id: toolCallId,
|
||||
toolCallId,
|
||||
toolName,
|
||||
input: args,
|
||||
dynamic: true,
|
||||
});
|
||||
}
|
||||
|
||||
lastIndex = pattern.lastIndex;
|
||||
}
|
||||
|
||||
// Keep the remaining buffer (unclosed tags) for the next chunk
|
||||
buffer = buffer.substring(lastIndex);
|
||||
|
||||
// If there's no open tag starting, we can flush the buffer as text
|
||||
if (!buffer.includes('<longcat_tool_call>')) {
|
||||
if (buffer) {
|
||||
controller.enqueue({ type: chunk.type, text: buffer, id: lastChunkId ?? chunk.type.includes('reasoning') ? `reasoning-${step}` : `text-${step}` });
|
||||
buffer = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
flush(controller) {
|
||||
if (buffer && lastChunkId && lastChunkType) {
|
||||
controller.enqueue({ type: `${lastChunkType}-delta`, text: buffer, id: lastChunkId });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -14,7 +14,11 @@ export default {
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createOpenAI({ name: 'ClosedRouter', apiKey, baseURL }),
|
||||
gateway: createOpenAI({
|
||||
name: 'ClosedRouter',
|
||||
apiKey,
|
||||
baseURL
|
||||
}),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
|
||||
+15
-25
@@ -1,31 +1,21 @@
|
||||
interface PendingRename {
|
||||
topicId: string;
|
||||
abortController: AbortController;
|
||||
}
|
||||
const pendingRenames = new Map<string, { attemptId: string; controller: AbortController }>();
|
||||
|
||||
const pendingRenames: Map<string, AbortController> = new Map();
|
||||
|
||||
export const cancelPendingRename = (topicId: string) => {
|
||||
const pendingRename = pendingRenames.get(topicId);
|
||||
if (pendingRename) {
|
||||
pendingRename.abort();
|
||||
pendingRenames.delete(topicId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const completeRename = (topicId: string) => {
|
||||
const controller = pendingRenames.get(topicId);
|
||||
if (controller) {
|
||||
export const cancelPendingRename = (topicId: string, attemptId?: string | null) => {
|
||||
const pending = pendingRenames.get(topicId);
|
||||
if (pending && pending.attemptId === attemptId) {
|
||||
pending.controller.abort();
|
||||
pendingRenames.delete(topicId);
|
||||
}
|
||||
};
|
||||
|
||||
export const addPendingRename = (topicId: string) => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
pendingRenames.set(topicId, abortController);
|
||||
return abortController;
|
||||
export const completeRename = (topicId: string, attemptId: string) => {
|
||||
if (pendingRenames.get(topicId)?.attemptId === attemptId) {
|
||||
pendingRenames.delete(topicId);
|
||||
}
|
||||
};
|
||||
|
||||
export const addPendingRename = (topicId: string, attemptId: string) => {
|
||||
const controller = new AbortController();
|
||||
pendingRenames.set(topicId, { attemptId, controller });
|
||||
return controller;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Result } from "../../types/result";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { Err, Ok } from "../../types/result";
|
||||
|
||||
const searchResponseSchema = z.object({
|
||||
results: z.array(z.object({
|
||||
title: z.string(),
|
||||
url: z.string(),
|
||||
content: z.string().optional().default(""),
|
||||
})),
|
||||
unresponsive_engines: z.array(z.tuple([z.string(), z.string()])).optional().default([]),
|
||||
});
|
||||
|
||||
export const searchInputSchema = z.object({
|
||||
query: z.string(),
|
||||
results: z.number().int().positive().describe("Number of search results to return; choose this for every search."),
|
||||
});
|
||||
|
||||
export const searchOutputSchema = z.object({
|
||||
results: z.array(z.object({
|
||||
title: z.string(),
|
||||
link: z.string(),
|
||||
sourceId: z.string().describe('Copy this exact ID into [[cite:ID]]. The literal "cite:" prefix is required; [[ID]] is invalid.').optional(),
|
||||
snippet: z.string(),
|
||||
})),
|
||||
warnings: z.array(z.object({ engine: z.string(), reason: z.string() })),
|
||||
});
|
||||
|
||||
type SearchOutput = z.infer<typeof searchOutputSchema>;
|
||||
|
||||
export const limitSearchResults = (output: SearchOutput, results: number): SearchOutput => ({
|
||||
results: output.results.slice(0, results),
|
||||
warnings: output.warnings,
|
||||
});
|
||||
|
||||
export const readSearchResponse = (body: unknown): Result<SearchOutput, string> => {
|
||||
const parsed = searchResponseSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return Err("Search returned an unexpected response format.");
|
||||
}
|
||||
const warnings = parsed.data.unresponsive_engines.map(([engine, reason]) => ({ engine, reason }));
|
||||
if (parsed.data.results.length === 0 && warnings.length > 0) {
|
||||
return Err(`Search returned no results because engines failed: ${warnings.map(warning => `${warning.engine}: ${warning.reason}`).join("; ")}`);
|
||||
}
|
||||
return Ok({
|
||||
results: parsed.data.results.map(item => ({
|
||||
sourceId: `s${randomUUID().replaceAll("-", "")}`,
|
||||
title: item.title,
|
||||
link: item.url,
|
||||
snippet: item.content,
|
||||
})),
|
||||
warnings,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { Err, Ok } from '../../types/result';
|
||||
import { serializeTopicSnapshot } from '../../utils/topic-sync';
|
||||
import { topicEvents } from './events';
|
||||
|
||||
interface TopicSnapshot {
|
||||
messages: { id: string; parts: any[] }[];
|
||||
}
|
||||
|
||||
export const createTopicSnapshotStream = (topicId: string, offeredFingerprint: unknown) => {
|
||||
let controller: ReadableStreamDefaultController | undefined;
|
||||
let closed = false;
|
||||
const buffered: string[] = [];
|
||||
const sink = {
|
||||
enqueue(frame: string) {
|
||||
if (closed) return;
|
||||
if (controller) controller.enqueue(frame);
|
||||
else buffered.push(frame);
|
||||
},
|
||||
};
|
||||
const close = () => {
|
||||
closed = true;
|
||||
topicEvents.unsubscribe(topicId, sink);
|
||||
buffered.length = 0;
|
||||
};
|
||||
|
||||
return {
|
||||
close,
|
||||
async open(readSnapshot: () => Promise<TopicSnapshot | undefined>) {
|
||||
if (closed) return Err('closed' as const);
|
||||
// Subscribe before reading: changes during the query must not be lost.
|
||||
topicEvents.subscribe(topicId, sink);
|
||||
let topic;
|
||||
try {
|
||||
topic = await readSnapshot();
|
||||
} catch (error) {
|
||||
close();
|
||||
console.error('Failed to read topic snapshot', error);
|
||||
return Err('read-failed' as const);
|
||||
}
|
||||
if (closed || !topic) {
|
||||
close();
|
||||
return Err('not-found' as const);
|
||||
}
|
||||
// Include deltas already emitted but not yet flushed to PostgreSQL.
|
||||
topicEvents.applyLiveText(topicId, topic.messages);
|
||||
const fingerprint = createHash('sha256').update(serializeTopicSnapshot(topic)).digest('hex');
|
||||
const matches = offeredFingerprint === fingerprint;
|
||||
|
||||
return Ok(new ReadableStream<string>({
|
||||
start(streamController) {
|
||||
controller = streamController;
|
||||
controller.enqueue(`data: ${JSON.stringify(matches
|
||||
? { type: 'synced', payload: { id: topicId } }
|
||||
: { type: 'initial_state', payload: topic })}\n\n`);
|
||||
for (const frame of buffered) {
|
||||
if (frame.startsWith('data: ')) {
|
||||
const replay = { ...JSON.parse(frame.slice(6)), replay: true };
|
||||
controller.enqueue(`data: ${JSON.stringify(replay)}\n\n`);
|
||||
} else {
|
||||
controller.enqueue(frame);
|
||||
}
|
||||
}
|
||||
buffered.length = 0;
|
||||
},
|
||||
cancel: close,
|
||||
}));
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { collectCitationSources } from "../app/utils/citations";
|
||||
import { createMarkdownParser, getNodeChildren, getNodeTag } from "../app/utils/markdown";
|
||||
import type { MarkdownAstNode } from "../app/utils/markdown";
|
||||
import { readSearchResponse } from "../server/utils/search";
|
||||
|
||||
const citations = (nodes: MarkdownAstNode[]): MarkdownAstNode[] => nodes.flatMap(node =>
|
||||
getNodeTag(node) === "source-cite" ? [node] : citations(getNodeChildren(node)),
|
||||
);
|
||||
|
||||
describe("citation parsing", () => {
|
||||
test("keeps citation markers as text unless explicitly enabled", async () => {
|
||||
const content = "Claim [[cite:1234]] and [[cite:s7,s9]].";
|
||||
for (const options of [{}, { citations: false }]) {
|
||||
for (const streaming of [false, true]) {
|
||||
const tree = await createMarkdownParser(options)(content, { streaming });
|
||||
expect(citations(tree.nodes)).toEqual([]);
|
||||
expect(getNodeChildren(tree.nodes[0] ?? "")).toEqual([content]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("renders single and grouped references as dedicated nodes", async () => {
|
||||
const tree = await createMarkdownParser({ citations: true })("Claim [[cite:s7]]. Another [[cite:s7,s9]].");
|
||||
expect(citations(tree.nodes)).toEqual([
|
||||
["source-cite", { ids: "s7" }],
|
||||
["source-cite", { ids: "s7,s9" }],
|
||||
]);
|
||||
});
|
||||
|
||||
test("does not interpret code, escaped references, or link labels", async () => {
|
||||
for (const content of [
|
||||
"`[[cite:s7]]`",
|
||||
"```text\n[[cite:s7]]\n```",
|
||||
"\\[[cite:s7]]",
|
||||
"[Claim [[cite:s7]]](https://example.com)",
|
||||
]) {
|
||||
const tree = await createMarkdownParser({ citations: true })(content);
|
||||
expect(citations(tree.nodes)).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test("streaming never auto-completes a citation", async () => {
|
||||
const parse = createMarkdownParser({ citations: true });
|
||||
const content = "Claim [[cite:s7,s9]]";
|
||||
for (let length = 1; length < content.length; length++) {
|
||||
expect(citations((await parse(content.slice(0, length), { streaming: true })).nodes)).toEqual([]);
|
||||
}
|
||||
expect(citations((await parse(content, { streaming: true })).nodes)).toHaveLength(1);
|
||||
expect(citations((await parse(content)).nodes)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("citation sources", () => {
|
||||
test("IDs survive storage, including duplicate URLs and multiple searches", () => {
|
||||
const first = readSearchResponse({ results: [
|
||||
{ title: "One", url: "https://example.com" },
|
||||
{ title: "Two", url: "https://example.com" },
|
||||
] });
|
||||
const second = readSearchResponse({ results: [{ title: "Three", url: "https://example.com" }] });
|
||||
expect(first.ok && second.ok).toBe(true);
|
||||
if (!first.ok || !second.ok) {
|
||||
return;
|
||||
}
|
||||
const parts = [first.data, second.data].map(output => ({
|
||||
toolCall: { toolName: "search", output: { type: "json", value: JSON.stringify(output) } },
|
||||
}));
|
||||
const sources = collectCitationSources(parts);
|
||||
expect(sources.size).toBe(3);
|
||||
expect(sources.get(first.data.results[0]?.sourceId ?? "")?.title).toBe("One");
|
||||
expect(collectCitationSources(JSON.parse(JSON.stringify(parts)))).toEqual(sources);
|
||||
expect(sources.has("invented")).toBe(false);
|
||||
expect(collectCitationSources([]).size).toBe(0);
|
||||
});
|
||||
|
||||
test("ignores legacy results, non-search tools, and unsafe URLs", () => {
|
||||
const output = { type: "json", value: JSON.stringify({ results: [
|
||||
{ title: "Legacy", link: "https://example.com" },
|
||||
{ sourceId: "bad", title: "Bad", link: "javascript:alert(1)" },
|
||||
] }) };
|
||||
expect(collectCitationSources([{ toolCall: { toolName: "search", output } }]).size).toBe(0);
|
||||
expect(collectCitationSources([{ toolCall: { toolName: "python", output } }]).size).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { getComposerHeight } from '../app/utils/composer-size';
|
||||
|
||||
describe('composer height stability', () => {
|
||||
test('subpixel noise never oscillates around the two-line minimum', () => {
|
||||
const heights = [47.99999, 48, 48.01].map(height => getComposerHeight(height, 24, 0, 0, true));
|
||||
expect(heights).toEqual([48, 48, 48]);
|
||||
});
|
||||
|
||||
test('grows and shrinks for actual lines, capped at ten lines', () => {
|
||||
expect([24, 48, 72, 264, 72, 24].map(height => getComposerHeight(height, 24, 0, 0, true)))
|
||||
.toEqual([48, 48, 72, 240, 72, 48]);
|
||||
});
|
||||
|
||||
test('accounts for padding and borders according to box sizing', () => {
|
||||
expect(getComposerHeight(80, 24, 8, 2, true)).toBe(82);
|
||||
expect(getComposerHeight(80, 24, 8, 2, false)).toBe(72);
|
||||
expect(getComposerHeight(10, 24, 8, 2, true)).toBe(58);
|
||||
});
|
||||
|
||||
test('respects fractional typography rather than hardcoding a 24px line', () => {
|
||||
expect(getComposerHeight(40, 20.25, 0, 0, true)).toBe(41);
|
||||
expect(getComposerHeight(1000, 20.25, 0, 0, true)).toBe(203);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { MarkdownTree } from '../app/utils/markdown';
|
||||
import { expect, test } from 'bun:test';
|
||||
import { splitParagraphBreaks } from '../app/utils/markdown';
|
||||
|
||||
test('paragraph splitting preserves nested formatting without mutating a cached parser tree', () => {
|
||||
const tree = { nodes: [
|
||||
['p', {}, 'before', ['br', {}], ['strong', {}, 'after']],
|
||||
['blockquote', {}, 'one', ['br', {}], ['em', {}, 'two']],
|
||||
] } as MarkdownTree;
|
||||
const original = structuredClone(tree);
|
||||
const result = splitParagraphBreaks(tree);
|
||||
expect(result.nodes).toEqual([
|
||||
['p', {}, 'before'],
|
||||
['p', {}, ['strong', {}, 'after']],
|
||||
['blockquote', {}, ['p', {}, 'one'], ['p', {}, ['em', {}, 'two']]],
|
||||
]);
|
||||
expect(tree).toEqual(original);
|
||||
expect(splitParagraphBreaks(tree)).toEqual(result);
|
||||
});
|
||||
|
||||
test('br-free trees return by reference with no allocation', () => {
|
||||
const tree = { nodes: [
|
||||
['p', {}, 'plain ', ['strong', {}, 'text']],
|
||||
['ul', {}, ['li', {}, 'one'], ['li', {}, 'two']],
|
||||
] } as MarkdownTree;
|
||||
expect(splitParagraphBreaks(tree)).toBe(tree);
|
||||
});
|
||||
|
||||
test('single newlines still split into sibling paragraphs', () => {
|
||||
const tree = { nodes: [
|
||||
['p', {}, 'line one', ['br', {}], 'line two'],
|
||||
] } as MarkdownTree;
|
||||
expect(splitParagraphBreaks(tree).nodes).toEqual([
|
||||
['p', {}, 'line one'],
|
||||
['p', {}, 'line two'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('nested changes rebuild only the affected path', () => {
|
||||
const tree = { nodes: [
|
||||
['blockquote', {}, ['p', {}, 'kept']],
|
||||
['p', {}, 'split', ['br', {}], 'me'],
|
||||
] } as MarkdownTree;
|
||||
const result = splitParagraphBreaks(tree);
|
||||
expect(result).not.toBe(tree);
|
||||
expect(result.nodes[0]).toBe(tree.nodes[0]);
|
||||
expect(result.nodes).toEqual([
|
||||
['blockquote', {}, ['p', {}, 'kept']],
|
||||
['p', {}, 'split'],
|
||||
['p', {}, 'me'],
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
buildFocusedMessageTree,
|
||||
buildMessageTree,
|
||||
getActiveMessage,
|
||||
getCurrentChildIndex,
|
||||
getMessageCount,
|
||||
getNextActiveChildId,
|
||||
} from "../utils/message";
|
||||
|
||||
describe("message focus and versioning", () => {
|
||||
test("reproduces and fixes the 3/2 focus bug after deleting root message and regenerating", () => {
|
||||
// Step 1: Generate initial assistant message (A)
|
||||
const msgA = {
|
||||
id: "msg_a",
|
||||
userId: "user_1",
|
||||
topicId: "topic_1",
|
||||
role: "assistant" as const,
|
||||
content: "Response 1",
|
||||
parentMessageId: null,
|
||||
generationId: null,
|
||||
activeChildId: null,
|
||||
deleted: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
parts: [],
|
||||
generation: null,
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
let tree = buildMessageTree([msgA]);
|
||||
expect(tree.length).toBe(1);
|
||||
expect(getMessageCount(tree[0]!)).toBe(1);
|
||||
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
|
||||
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_a");
|
||||
|
||||
// Step 2: Regenerate the message -> creates child B, activeChildId points to B
|
||||
const msgB = {
|
||||
...msgA,
|
||||
id: "msg_b",
|
||||
content: "Response 2",
|
||||
parentMessageId: "msg_a",
|
||||
};
|
||||
msgA.activeChildId = "msg_b";
|
||||
|
||||
tree = buildMessageTree([msgA, msgB]);
|
||||
expect(tree.length).toBe(1);
|
||||
expect(tree[0]!.children.length).toBe(1);
|
||||
expect(getMessageCount(tree[0]!)).toBe(2);
|
||||
// Variant 2 of 2: index should be 1 (displayed as 1 + 1 = 2)
|
||||
expect(getCurrentChildIndex(tree[0]!)).toBe(1);
|
||||
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_b");
|
||||
|
||||
// Step 3: Switch focus to first, root message (A)
|
||||
const prevId = getNextActiveChildId(tree[0]!, -1);
|
||||
expect(prevId).toBeNull();
|
||||
msgA.activeChildId = null;
|
||||
|
||||
tree = buildMessageTree([msgA, msgB]);
|
||||
expect(getMessageCount(tree[0]!)).toBe(2);
|
||||
// Variant 1 of 2: index should be 0 (displayed as 0 + 1 = 1)
|
||||
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
|
||||
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_a");
|
||||
|
||||
// Step 4: Delete the root message (A)
|
||||
// Root is soft-deleted, active child switches to B
|
||||
msgA.deleted = true;
|
||||
msgA.activeChildId = "msg_b";
|
||||
|
||||
tree = buildMessageTree([msgA, msgB]);
|
||||
expect(tree.length).toBe(1);
|
||||
expect(tree[0]!.deleted).toBe(true);
|
||||
expect(getMessageCount(tree[0]!)).toBe(1);
|
||||
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
|
||||
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_b");
|
||||
|
||||
// Step 5: Regenerate the message -> creates child C with parentMessageId: msg_a
|
||||
const msgC = {
|
||||
...msgA,
|
||||
id: "msg_c",
|
||||
content: "Response 3",
|
||||
parentMessageId: "msg_a",
|
||||
deleted: false,
|
||||
};
|
||||
msgA.activeChildId = "msg_c";
|
||||
|
||||
tree = buildMessageTree([msgA, msgB, msgC]);
|
||||
expect(tree.length).toBe(1);
|
||||
expect(tree[0]!.children.length).toBe(2);
|
||||
expect(getMessageCount(tree[0]!)).toBe(2);
|
||||
// Crucial test: When C (second child) is active, index must be 1 (displayed as 2/2, NOT 3/2!)
|
||||
expect(getCurrentChildIndex(tree[0]!)).toBe(1);
|
||||
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_c");
|
||||
|
||||
// Verify switching focus back to B works seamlessly
|
||||
const backToB = getNextActiveChildId(tree[0]!, -1);
|
||||
expect(backToB).toBe("msg_b");
|
||||
msgA.activeChildId = backToB;
|
||||
|
||||
tree = buildMessageTree([msgA, msgB, msgC]);
|
||||
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
|
||||
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_b");
|
||||
|
||||
// Verify switching forward to C works seamlessly
|
||||
const forwardToC = getNextActiveChildId(tree[0]!, 1);
|
||||
expect(forwardToC).toBe("msg_c");
|
||||
msgA.activeChildId = forwardToC;
|
||||
|
||||
tree = buildMessageTree([msgA, msgB, msgC]);
|
||||
expect(getCurrentChildIndex(tree[0]!)).toBe(1);
|
||||
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_c");
|
||||
});
|
||||
|
||||
test("buildFocusedMessageTree excludes soft-deleted root messages", () => {
|
||||
const root = {
|
||||
id: "root",
|
||||
userId: "user_1",
|
||||
topicId: "topic_1",
|
||||
role: "assistant" as const,
|
||||
content: "Root",
|
||||
parentMessageId: null,
|
||||
generationId: null,
|
||||
activeChildId: "child_1",
|
||||
deleted: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
parts: [],
|
||||
generation: null,
|
||||
attachments: [],
|
||||
};
|
||||
const child1 = {
|
||||
...root,
|
||||
id: "child_1",
|
||||
content: "Child 1",
|
||||
parentMessageId: "root",
|
||||
activeChildId: null,
|
||||
deleted: false,
|
||||
};
|
||||
|
||||
const tree = buildMessageTree([root, child1]);
|
||||
const focused = buildFocusedMessageTree(tree);
|
||||
|
||||
expect(focused.length).toBe(1);
|
||||
expect(focused[0]!.id).toBe("child_1");
|
||||
expect(focused[0]!.deleted).toBe(false);
|
||||
});
|
||||
|
||||
test("buildMessageTree filters out deleted roots with no children", () => {
|
||||
const deletedEmptyRoot = {
|
||||
id: "deleted_root",
|
||||
userId: "user_1",
|
||||
topicId: "topic_1",
|
||||
role: "assistant" as const,
|
||||
content: "Empty",
|
||||
parentMessageId: null,
|
||||
generationId: null,
|
||||
activeChildId: null,
|
||||
deleted: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
parts: [],
|
||||
generation: null,
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
const tree = buildMessageTree([deletedEmptyRoot]);
|
||||
expect(tree.length).toBe(0);
|
||||
});
|
||||
|
||||
test("getNextActiveChildId clamps to bounds and handles null fallback", () => {
|
||||
const root = {
|
||||
id: "root",
|
||||
deleted: true,
|
||||
activeChildId: null as string | null,
|
||||
children: [
|
||||
{ id: "child_a" },
|
||||
{ id: "child_b" },
|
||||
],
|
||||
};
|
||||
|
||||
// If activeChildId is null and root is deleted, going left stays at child_a
|
||||
expect(getNextActiveChildId(root, -1)).toBe("child_a");
|
||||
// Going right moves to child_b
|
||||
expect(getNextActiveChildId(root, 1)).toBe("child_b");
|
||||
|
||||
// When at child_b, going right clamps to child_b
|
||||
root.activeChildId = "child_b";
|
||||
expect(getNextActiveChildId(root, 1)).toBe("child_b");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { limitSearchResults, readSearchResponse, searchInputSchema, searchOutputSchema } from "../server/utils/search";
|
||||
import { readSearchSources, readSearchWarnings } from "../app/utils/tool-rendering";
|
||||
|
||||
const site = { title: "Example", url: "https://example.com", content: "Example content" };
|
||||
|
||||
describe("search invocation count", () => {
|
||||
test("requires a positive integer on every invocation", () => {
|
||||
expect(searchInputSchema.safeParse({ query: "example" }).success).toBe(false);
|
||||
for (const results of [0, -1, 1.5, "2"]) {
|
||||
expect(searchInputSchema.safeParse({ query: "example", results }).success).toBe(false);
|
||||
}
|
||||
expect(searchInputSchema.safeParse({ query: "example", results: 2 }).success).toBe(true);
|
||||
});
|
||||
|
||||
test("slices ranked search results without dropping warnings", () => {
|
||||
const output = {
|
||||
results: [
|
||||
{ title: "First", link: "https://first.example", snippet: "" },
|
||||
{ title: "Second", link: "https://second.example", snippet: "" },
|
||||
],
|
||||
warnings: [{ engine: "google", reason: "CAPTCHA" }],
|
||||
};
|
||||
expect(limitSearchResults(output, 1)).toEqual({
|
||||
results: [output.results[0]],
|
||||
warnings: output.warnings,
|
||||
});
|
||||
expect(limitSearchResults(output, 3)).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("search engine failures", () => {
|
||||
test("returns an error when empty results accompany engine failures", () => {
|
||||
const result = readSearchResponse({
|
||||
results: [],
|
||||
unresponsive_engines: [["google", "Too many requests"], ["bing", "Suspended: Too many requests"]],
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain("google: Too many requests");
|
||||
expect(result.error).toContain("bing: Suspended: Too many requests");
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps partial results successful with renderable warnings", () => {
|
||||
const result = readSearchResponse({ results: [site], unresponsive_engines: [["google", "CAPTCHA"]] });
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(searchOutputSchema.safeParse(result.data).success).toBe(true);
|
||||
expect(readSearchSources(result.data)?.[0]?.url).toBe(site.url + "/");
|
||||
expect(readSearchWarnings(result.data)).toEqual([{ engine: "google", reason: "CAPTCHA" }]);
|
||||
}
|
||||
});
|
||||
|
||||
test("accepts genuinely empty results without engine failures", () => {
|
||||
const result = readSearchResponse({ results: [] });
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data).toEqual({ results: [], warnings: [] });
|
||||
}
|
||||
});
|
||||
|
||||
test("accepts results without snippets or failures", () => {
|
||||
const result = readSearchResponse({ results: [{ title: site.title, url: site.url }] });
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data.results[0]?.snippet).toBe("");
|
||||
expect(result.data.warnings).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects malformed responses", () => {
|
||||
expect(readSearchResponse({ error: "Invalid query" }).ok).toBe(false);
|
||||
});
|
||||
|
||||
test("still renders legacy arrays and ignores malformed warnings", () => {
|
||||
const legacy = [{ title: site.title, link: site.url, snippet: site.content }];
|
||||
expect(readSearchSources(legacy)).toEqual(readSearchSources({ results: legacy }));
|
||||
expect(readSearchWarnings(legacy)).toEqual([]);
|
||||
expect(readSearchWarnings({ warnings: [null, {}, { engine: "google", reason: 429 }] })).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readStoredToolError, serializeToolError } from "../utils/tool-errors";
|
||||
|
||||
const validationError = "Invalid input for tool search: Type validation failed: Value: {\"query\":123}.\nError message: [{\"code\":\"custom\"}]";
|
||||
|
||||
test("tool Error instances persist their message, not an empty JSON object", () => {
|
||||
expect(serializeToolError(new Error(validationError))).toBe(validationError);
|
||||
expect(serializeToolError(validationError)).toBe(validationError);
|
||||
expect(serializeToolError({ reason: "invalid input" })).toBe('{"reason":"invalid input"}');
|
||||
});
|
||||
|
||||
test("text validation errors remain text on history replay", () => {
|
||||
expect(readStoredToolError({ type: "text", value: validationError })).toEqual({
|
||||
type: "error-text",
|
||||
value: validationError,
|
||||
});
|
||||
});
|
||||
|
||||
test("legacy mislabeled JSON errors cannot break subsequent prompts", () => {
|
||||
expect(readStoredToolError({ type: "json", value: validationError })).toEqual({
|
||||
type: "error-text",
|
||||
value: validationError,
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves actual JSON errors and handles missing errors", () => {
|
||||
expect(readStoredToolError({ type: "json", value: '{"reason":"bad input"}' })).toEqual({
|
||||
type: "error-json",
|
||||
value: { reason: "bad input" },
|
||||
});
|
||||
expect(readStoredToolError(null).type).toBe("error-text");
|
||||
expect(readStoredToolError(undefined).type).toBe("error-text");
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
domainInitial,
|
||||
formatDuration,
|
||||
formatToolValue,
|
||||
getDomainColor,
|
||||
getFaviconUrl,
|
||||
getToolDisplayInfo,
|
||||
parseToolValue,
|
||||
readSearchSources,
|
||||
} from "../app/utils/tool-rendering";
|
||||
|
||||
describe("stored tool values", () => {
|
||||
test("handles missing, text, valid JSON and streaming JSON", () => {
|
||||
expect(parseToolValue(null)).toBeNull();
|
||||
expect(parseToolValue({ type: "text", value: "hello" })).toBe("hello");
|
||||
expect(parseToolValue({ type: "json", value: '{"query":"gold"}' })).toEqual({ query: "gold" });
|
||||
expect(formatToolValue({ type: "json", value: '{"query":' })).toBe('{"query":');
|
||||
});
|
||||
});
|
||||
|
||||
describe("search sources", () => {
|
||||
test("distinguishes unavailable results from empty results", () => {
|
||||
expect(readSearchSources(null)).toBeNull();
|
||||
expect(readSearchSources("partial")).toBeNull();
|
||||
expect(readSearchSources([])).toEqual([]);
|
||||
});
|
||||
|
||||
test("rejects unsafe links, tolerates malformed entries and deduplicates pages", () => {
|
||||
expect(readSearchSources([
|
||||
null,
|
||||
{ link: "javascript:alert(1)" },
|
||||
{ link: "data:text/html,hello" },
|
||||
{ link: "/relative" },
|
||||
{ link: "https://example.com/page#one", title: "Example", snippet: "Evidence" },
|
||||
{ link: "https://example.com/page#two", title: "Duplicate" },
|
||||
{ link: "https://other.example/", title: 1 },
|
||||
])).toEqual([
|
||||
{ url: "https://example.com/page", hostname: "example.com", title: "Example", snippet: "Evidence" },
|
||||
{ url: "https://other.example/", hostname: "other.example", title: "other.example", snippet: "" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("duration formatting", () => {
|
||||
test("formats duration in seconds with 1 decimal", () => {
|
||||
const start = new Date("2026-09-25T12:00:00.000Z");
|
||||
const end = new Date("2026-09-25T12:00:02.100Z");
|
||||
expect(formatDuration(start, end)).toBe("2.1s");
|
||||
});
|
||||
|
||||
test("formats numeric timestamps", () => {
|
||||
const now = 1700000000000;
|
||||
expect(formatDuration(now, now + 1500)).toBe("1.5s");
|
||||
expect(formatDuration(new Date(now).toISOString(), now + 3200)).toBe("3.2s");
|
||||
});
|
||||
|
||||
test("formats short duration", () => {
|
||||
const start = new Date("2026-09-25T12:00:00.000Z");
|
||||
const end = new Date("2026-09-25T12:00:00.400Z");
|
||||
expect(formatDuration(start, end)).toBe("0.4s");
|
||||
});
|
||||
|
||||
test("returns null for invalid or negative or sub-threshold duration", () => {
|
||||
expect(formatDuration(null, null)).toBeNull();
|
||||
const start = new Date("2026-09-25T12:00:02.000Z");
|
||||
const end = new Date("2026-09-25T12:00:01.000Z");
|
||||
expect(formatDuration(start, end)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("tool display info", () => {
|
||||
test("handles search with results", () => {
|
||||
const info = getToolDisplayInfo({
|
||||
toolName: "search",
|
||||
status: "completed",
|
||||
input: { type: "json", value: '{"query":"drizzle pg enum best practices"}' },
|
||||
output: {
|
||||
type: "json",
|
||||
value: JSON.stringify([
|
||||
{ link: "https://orm.drizzle.team/docs/enum", title: "PostgreSQL enums | Drizzle ORM" },
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(info.verb).toBe("Searched 1 site");
|
||||
expect(info.target).toBe('"drizzle pg enum best practices"');
|
||||
expect(info.icon).toBe("i-mynaui-search");
|
||||
});
|
||||
|
||||
test("handles read file", () => {
|
||||
const info = getToolDisplayInfo({
|
||||
toolName: "read_file",
|
||||
status: "completed",
|
||||
input: { type: "json", value: '{"path":"drizzle/schema.ts"}' },
|
||||
});
|
||||
expect(info.verb).toBe("Read");
|
||||
expect(info.target).toBe("drizzle/schema.ts");
|
||||
expect(info.icon).toBe("i-mynaui-file");
|
||||
});
|
||||
|
||||
test("handles file edit pending", () => {
|
||||
const info = getToolDisplayInfo({
|
||||
toolName: "edit_file",
|
||||
status: "pending",
|
||||
input: { type: "json", value: '{"path":"server/api/chat/generate.ts"}' },
|
||||
});
|
||||
expect(info.verb).toBe("Editing");
|
||||
expect(info.target).toBe("server/api/chat/generate.ts");
|
||||
expect(info.icon).toBe("i-mynaui-edit");
|
||||
});
|
||||
|
||||
test("handles query failed", () => {
|
||||
const info = getToolDisplayInfo({
|
||||
toolName: "query",
|
||||
status: "failed",
|
||||
input: { type: "json", value: '{"query":"select * from tool_calls"}' },
|
||||
error: { type: "text", value: 'relation "tool_calls" does not exist' },
|
||||
});
|
||||
expect(info.verb).toBe("Query failed");
|
||||
expect(info.target).toBe("select * from tool_calls");
|
||||
expect(info.icon).toBe("i-mynaui-database");
|
||||
});
|
||||
|
||||
test("handles fetch cancelled", () => {
|
||||
const info = getToolDisplayInfo({
|
||||
toolName: "fetchUrl",
|
||||
status: "cancelled",
|
||||
input: { type: "json", value: '{"url":"https://orm.drizzle.team/docs/..."}' },
|
||||
});
|
||||
expect(info.verb).toBe("Fetch cancelled");
|
||||
expect(info.target).toBe("https://orm.drizzle.team/docs/...");
|
||||
expect(info.icon).toBe("i-mynaui-link");
|
||||
});
|
||||
|
||||
test("handles python execution", () => {
|
||||
const info = getToolDisplayInfo({
|
||||
toolName: "python",
|
||||
status: "completed",
|
||||
input: { type: "json", value: '{"code":"2 + 2"}' },
|
||||
output: { type: "json", value: '{"output":"4"}' },
|
||||
});
|
||||
expect(info.verb).toBe("Ran Python");
|
||||
expect(info.target).toBe("2 + 2");
|
||||
expect(info.icon).toBe("i-mynaui-terminal");
|
||||
});
|
||||
});
|
||||
|
||||
describe("domain utilities", () => {
|
||||
test("extracts domain initial correctly", () => {
|
||||
expect(domainInitial("goldprice.org")).toBe("G");
|
||||
expect(domainInitial("www.kitco.com")).toBe("K");
|
||||
expect(domainInitial("apmex.com")).toBe("A");
|
||||
});
|
||||
|
||||
test("returns consistent domain colors", () => {
|
||||
const color1 = getDomainColor("goldprice.org");
|
||||
const color2 = getDomainColor("goldprice.org");
|
||||
expect(color1).toBe(color2);
|
||||
expect(typeof color1).toBe("string");
|
||||
expect(color1.startsWith("#")).toBe(true);
|
||||
});
|
||||
|
||||
test("formats DuckDuckGo favicon service url", () => {
|
||||
expect(getFaviconUrl("goldprice.org")).toBe("https://icons.duckduckgo.com/ip3/goldprice.org.ico");
|
||||
expect(getFaviconUrl("www.kitco.com")).toBe("https://icons.duckduckgo.com/ip3/kitco.com.ico");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { addPendingRename, cancelPendingRename, completeRename } from "../server/utils/renames";
|
||||
import { provisionalTopicTitle } from "../utils/topic-title";
|
||||
|
||||
describe("rename attempt ownership", () => {
|
||||
test("an old completion cannot remove a newer attempt", () => {
|
||||
addPendingRename("topic", "old");
|
||||
const current = addPendingRename("topic", "new");
|
||||
completeRename("topic", "old");
|
||||
cancelPendingRename("topic", "new");
|
||||
expect(current.signal.aborted).toBe(true);
|
||||
});
|
||||
|
||||
test("an old cancellation cannot abort a newer attempt", () => {
|
||||
const current = addPendingRename("topic", "new");
|
||||
cancelPendingRename("topic", "old");
|
||||
expect(current.signal.aborted).toBe(false);
|
||||
completeRename("topic", "new");
|
||||
});
|
||||
|
||||
test("cancellation is safe after completion or restart", () => {
|
||||
cancelPendingRename("missing", "old");
|
||||
const current = addPendingRename("topic", "new");
|
||||
completeRename("topic", "new");
|
||||
cancelPendingRename("topic", "new");
|
||||
expect(current.signal.aborted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("provisional titles", () => {
|
||||
test("uses the first meaningful line and normalizes whitespace", () => {
|
||||
expect(provisionalTopicTitle({ content: "\n Help me debug this\nMore details" })).toBe("Help me debug this");
|
||||
});
|
||||
test("names attachment-only conversations", () => {
|
||||
expect(provisionalTopicTitle({ content: "", files: [{ name: "photo.jpg" }] })).toBe("photo.jpg");
|
||||
expect(provisionalTopicTitle({ content: "", fileIds: ["image"] })).toBe("Attached files");
|
||||
});
|
||||
test("bounds title length and handles empty input", () => {
|
||||
expect(provisionalTopicTitle({ content: "a".repeat(200) })).toHaveLength(100);
|
||||
expect(provisionalTopicTitle()).toBe("New Topic");
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user