6ee4087a29
- Centralize `useAgents` and `useModels` state within the Nuxt app context to prevent data leaks and improve initialization. - Migrate virtualization from `vue-virtual-scroller` to `@tanstack/vue-virtual` with new `RowVirtualizerFixed` and `RowVirtualizerDynamic` components. - Upgrade Nuxt to v4.3.1 and remove `@vue-macros/nuxt`. - Replace `big.js` with an optimized custom `lshDecimal` string manipulation logic for pricing calculations in the provider API. - Implement automatic focus redirection in `ChatInput` to capture standard keyboard input. - Refactor Sidenav and Settings components to utilize virtualization for long lists (topics, agents, models). - Enhance theme colors and mobile experience. More work to come on both of these.
131 lines
4.0 KiB
Vue
131 lines
4.0 KiB
Vue
<script setup lang="ts">
|
|
import { type Grammar } from 'shiki';
|
|
import { hashSync } from '~/utils/hash';
|
|
const props = defineProps<{ code: string; language: string }>();
|
|
|
|
const start = Date.now();
|
|
|
|
const renderId = `${useId()}-${hashSync(props.code + props.language)}`;
|
|
const copied = ref(false);
|
|
const collapsed = ref(false);
|
|
|
|
const { $shiki } = useNuxtApp();
|
|
|
|
const { data: parsed, clear } = await useAsyncData(`shiki-${renderId}`,
|
|
() => parseCode(props.code, props.language.toLowerCase()),
|
|
{
|
|
watch: [() => props.code],
|
|
dedupe: 'defer'
|
|
}
|
|
);
|
|
|
|
const lineNumberWidth = computed(() => {
|
|
if (!parsed.value?.html) return 1;
|
|
// Count newlines in the generated HTML or the source code
|
|
// Using props.code is safer and faster than parsing the HTML string
|
|
return props.code.split('\n').length.toString().length;
|
|
});
|
|
|
|
async function parseCode(code: string, lang: string) {
|
|
let displayLang = lang;
|
|
|
|
try {
|
|
let shikiLang = await $shiki.getLanguage(lang);
|
|
|
|
displayLang = (shikiLang as unknown as Grammar).name;
|
|
} catch {
|
|
lang = 'text';
|
|
}
|
|
|
|
let html = await $shiki.codeToHtml(code.trim(), {
|
|
lang,
|
|
themes: { dark: 'vitesse-dark', light: 'vitesse-light' },
|
|
});
|
|
|
|
return { html, displayLang };
|
|
}
|
|
|
|
let copyTimeout: NodeJS.Timeout | null = null;
|
|
function copyCode() {
|
|
copied.value = true;
|
|
navigator.clipboard.writeText(props.code);
|
|
|
|
if (copyTimeout) clearTimeout(copyTimeout);
|
|
copyTimeout = setTimeout(() => {
|
|
copied.value = false;
|
|
copyTimeout = null;
|
|
}, 2000);
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
if (copyTimeout) clearTimeout(copyTimeout);
|
|
clear();
|
|
});
|
|
|
|
console.log("shiki codeblock rendered in", Date.now() - start);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col my-2 rounded-xl overflow-hidden">
|
|
<div class="flex items-center pl-3 pr-1.5 py-1.5 text-sm font-sans bg-[var(--color-hover)] justify-between">
|
|
<div class="capitalize">
|
|
{{ parsed?.displayLang }}
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button @click="copyCode()"
|
|
class="flex items-center px-1 gap-0.5 rounded-md hover:bg-[var(--color-hover)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
|
Copy
|
|
<Icon v-if="!copied" name="mynaui:copy" class="text-4 text-[var(--text-secondary)]" />
|
|
<Icon v-else name="mynaui:check" class="text-4 text-emerald-500" />
|
|
</button>
|
|
|
|
<button @click="collapsed = !collapsed"
|
|
class="flex items-center justify-center h-5.5 w-5.5 rounded-md hover:bg-[var(--color-hover)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
|
<Icon name="mynaui:chevron-down"
|
|
:class="['text-4 h-4 w-4 text-[var(--text-secondary)] transition-transform duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]', collapsed ? '-rotate-90' : '']" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="grid transition-all duration-350 ease-in-out"
|
|
:class="collapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'"
|
|
:style="`--line-number-width: ${lineNumberWidth}ch;`" :id="`code-${renderId}`">
|
|
<div class="overflow-hidden code-container" v-html="parsed?.html"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
.code-container>pre {
|
|
overflow: auto hidden;
|
|
min-height: 0;
|
|
scrollbar-width: thin;
|
|
padding: 1rem;
|
|
line-height: 0;
|
|
counter-reset: lines;
|
|
}
|
|
|
|
.code-container>pre>code .line::before {
|
|
counter-increment: lines;
|
|
content: counter(lines);
|
|
width: var(--line-number-width);
|
|
margin-right: 1.5rem;
|
|
display: inline-block;
|
|
text-align: right;
|
|
}
|
|
|
|
.dark .code-container>pre>code .line::before {
|
|
color: rgba(255, 255, 255, 0.25);
|
|
}
|
|
|
|
.light .code-container>pre>code .line::before {
|
|
color: rgba(0, 0, 0, 0.25);
|
|
}
|
|
|
|
.code-container>pre>code {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 12px;
|
|
line-height: 0;
|
|
font-variant-ligatures: none;
|
|
}
|
|
</style>
|