Skip to content

Commit 06da9b9

Browse files
peoraycwandev
andauthored
feat: add context component (#32)
Co-authored-by: Charlie Wang <18888351756@163.com>
1 parent cc96c99 commit 06da9b9

21 files changed

+1288
-17
lines changed

apps/www/content/3.components/1.chatbot/context.md

Lines changed: 700 additions & 0 deletions
Large diffs are not rendered by default.

apps/www/plugins/ai-elements.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Checkpoint,
88
CodeBlock,
99
CodeBlockDark,
10+
Context,
1011
Confirmation,
1112
ConfirmationAccepted,
1213
ConfirmationRejected,
@@ -78,6 +79,7 @@ export default defineNuxtPlugin((nuxtApp) => {
7879
vueApp.component('CodeBlockDark', CodeBlockDark)
7980
vueApp.component('Checkpoint', Checkpoint)
8081
vueApp.component('Workflow', Workflow)
82+
vueApp.component('Context', Context)
8183
vueApp.component('Confirmation', Confirmation)
8284
vueApp.component('ConfirmationAccepted', ConfirmationAccepted)
8385
vueApp.component('ConfirmationRejected', ConfirmationRejected)

packages/elements/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"motion-v": "^1.7.3",
1919
"shiki": "^3.14.0",
2020
"streamdown-vue": "^1.0.21",
21+
"tokenlens": "^1.3.1",
2122
"vue": "^3.5.22",
2223
"vue-stick-to-bottom": "^0.1.0"
2324
},
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script setup lang="ts">
2+
import type { LanguageModelUsage } from 'ai'
3+
import type { ModelId } from './context'
4+
import { HoverCard } from '@repo/shadcn-vue/components/ui/hover-card'
5+
import { computed, provide } from 'vue'
6+
import { ContextKey } from './context'
7+
8+
interface Props {
9+
usedTokens: number
10+
maxTokens: number
11+
usage?: LanguageModelUsage
12+
modelId?: ModelId
13+
}
14+
15+
const props = defineProps<Props>()
16+
17+
provide(ContextKey, {
18+
usedTokens: computed(() => props.usedTokens),
19+
maxTokens: computed(() => props.maxTokens),
20+
usage: computed(() => props.usage),
21+
modelId: computed(() => props.modelId),
22+
})
23+
</script>
24+
25+
<template>
26+
<HoverCard :close-delay="0" :open-delay="0" v-bind="{ ...$attrs, ...props }">
27+
<slot />
28+
</HoverCard>
29+
</template>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@repo/shadcn-vue/lib/utils'
4+
import { getUsage } from 'tokenlens'
5+
import { computed, useSlots } from 'vue'
6+
import { useContextValue } from './context'
7+
import TokensWithCost from './TokensWithCost.vue'
8+
9+
const props = defineProps<{
10+
class?: HTMLAttributes['class']
11+
}>()
12+
13+
const { usage, modelId } = useContextValue()
14+
const slots = useSlots()
15+
16+
const cacheTokens = computed(() => usage.value?.cachedInputTokens ?? 0)
17+
18+
const cacheCostText = computed(() => {
19+
if (!modelId.value || !cacheTokens.value)
20+
return undefined
21+
22+
const cacheCost = getUsage({
23+
modelId: modelId.value,
24+
usage: { cacheReads: cacheTokens.value, input: 0, output: 0 },
25+
}).costUSD?.totalUSD
26+
27+
return new Intl.NumberFormat('en-US', {
28+
style: 'currency',
29+
currency: 'USD',
30+
}).format(cacheCost ?? 0)
31+
})
32+
</script>
33+
34+
<template>
35+
<slot v-if="slots.default" />
36+
<div
37+
v-else-if="cacheTokens > 0"
38+
:class="
39+
cn('flex items-center justify-between text-xs', props.class)
40+
"
41+
v-bind="$attrs"
42+
>
43+
<span class="text-muted-foreground">Cache</span>
44+
<TokensWithCost :cost-text="cacheCostText" :tokens="cacheTokens" />
45+
</div>
46+
</template>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { HoverCardContent } from '@repo/shadcn-vue/components/ui/hover-card'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
6+
const props = defineProps<{
7+
class?: HTMLAttributes['class']
8+
}>()
9+
</script>
10+
11+
<template>
12+
<HoverCardContent
13+
:class="
14+
cn('min-w-60 divide-y overflow-hidden p-0', props.class)
15+
"
16+
v-bind="$attrs"
17+
>
18+
<slot />
19+
</HoverCardContent>
20+
</template>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@repo/shadcn-vue/lib/utils'
4+
5+
const props = defineProps<{
6+
class?: HTMLAttributes['class']
7+
}>()
8+
</script>
9+
10+
<template>
11+
<div :class="cn('w-full p-3', props.class)" v-bind="$attrs">
12+
<slot />
13+
</div>
14+
</template>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@repo/shadcn-vue/lib/utils'
4+
import { getUsage } from 'tokenlens'
5+
import { computed, useSlots } from 'vue'
6+
import { useContextValue } from './context'
7+
8+
const props = defineProps<{
9+
class?: HTMLAttributes['class']
10+
}>()
11+
12+
const { modelId, usage } = useContextValue()
13+
const slots = useSlots()
14+
15+
const totalCost = computed(() => {
16+
if (!modelId.value)
17+
return 0
18+
19+
const costUSD = getUsage({
20+
modelId: modelId.value,
21+
usage: {
22+
input: usage.value?.inputTokens ?? 0,
23+
output: usage.value?.outputTokens ?? 0,
24+
},
25+
}).costUSD?.totalUSD
26+
27+
return new Intl.NumberFormat('en-US', {
28+
style: 'currency',
29+
currency: 'USD',
30+
}).format(costUSD ?? 0)
31+
})
32+
</script>
33+
34+
<template>
35+
<div
36+
:class="
37+
cn(
38+
'flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs',
39+
props.class,
40+
)
41+
"
42+
>
43+
<slot v-if="slots.default" />
44+
45+
<template v-else>
46+
<span class="text-muted-foreground">Total cost</span>
47+
<span>{{ totalCost }}</span>
48+
</template>
49+
</div>
50+
</template>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { Progress } from '@repo/shadcn-vue/components/ui/progress'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
import { computed, useSlots } from 'vue'
6+
import { useContextValue } from './context'
7+
8+
const props = defineProps<{
9+
class?: HTMLAttributes['class']
10+
}>()
11+
12+
const PERCENT_MAX = 100
13+
14+
const { usedTokens, maxTokens } = useContextValue()
15+
const slots = useSlots()
16+
17+
const formatter = new Intl.NumberFormat('en-US', { notation: 'compact' })
18+
19+
const usedPercent = computed(() => {
20+
if (maxTokens.value === 0)
21+
return 0
22+
return usedTokens.value / maxTokens.value
23+
})
24+
const displayPct = computed(() => {
25+
return new Intl.NumberFormat('en-US', {
26+
style: 'percent',
27+
maximumFractionDigits: 1,
28+
}).format(usedPercent.value)
29+
})
30+
const used = computed(() => formatter.format(usedTokens.value))
31+
const total = computed(() => formatter.format(maxTokens.value))
32+
</script>
33+
34+
<template>
35+
<div :class="cn('w-full space-y-2 p-3', props.class)">
36+
<slot v-if="slots.default" />
37+
38+
<template v-else>
39+
<div class="flex items-center justify-between gap-3 text-xs">
40+
<p>{{ displayPct }}</p>
41+
<p class="font-mono text-muted-foreground">
42+
{{ used }} / {{ total }}
43+
</p>
44+
</div>
45+
<div class="space-y-2">
46+
<Progress class="bg-muted" :model-value="usedPercent * PERCENT_MAX" />
47+
</div>
48+
</template>
49+
</div>
50+
</template>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { useContextValue } from './context'
4+
5+
const ICON_RADIUS = 10
6+
const ICON_VIEWBOX = 24
7+
const ICON_CENTER = 12
8+
const ICON_STROKE_WIDTH = 2
9+
10+
const { usedTokens, maxTokens } = useContextValue()
11+
12+
const circumference = 2 * Math.PI * ICON_RADIUS
13+
14+
const usedPercent = computed(() => {
15+
if (maxTokens.value === 0)
16+
return 0
17+
return usedTokens.value / maxTokens.value
18+
})
19+
20+
const dashOffset = computed(() => {
21+
return circumference * (1 - usedPercent.value)
22+
})
23+
24+
const svgStyle = {
25+
transformOrigin: 'center',
26+
transform: 'rotate(-90deg)',
27+
}
28+
</script>
29+
30+
<template>
31+
<svg
32+
aria-label="Model context usage"
33+
height="20"
34+
role="img"
35+
style="color: currentcolor"
36+
:viewBox="`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`"
37+
width="20"
38+
>
39+
<circle
40+
:cx="ICON_CENTER"
41+
:cy="ICON_CENTER"
42+
fill="none"
43+
opacity="0.25"
44+
:r="ICON_RADIUS"
45+
stroke="currentColor"
46+
:stroke-width="ICON_STROKE_WIDTH"
47+
/>
48+
<circle
49+
:cx="ICON_CENTER"
50+
:cy="ICON_CENTER"
51+
fill="none"
52+
opacity="0.7"
53+
:r="ICON_RADIUS"
54+
stroke="currentColor"
55+
:stroke-dasharray="`${circumference} ${circumference}`"
56+
:stroke-dashoffset="dashOffset"
57+
stroke-linecap="round"
58+
:stroke-width="ICON_STROKE_WIDTH"
59+
:style="svgStyle"
60+
/>
61+
</svg>
62+
</template>

0 commit comments

Comments
 (0)