Skip to content

Commit aff549b

Browse files
committed
feat: elegant BYOK UI settings support ✨
1 parent 6c777e6 commit aff549b

26 files changed

Lines changed: 529 additions & 4 deletions

apps/frontend/app/components/chat/interface/AgentSelector.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<script setup lang="ts">
22
const chatContext = useChatContext()
3+
const enabledProviders = computed(() =>
4+
Object.entries(chatContext.agentsSetting.value.providers).filter(([_, v]) => v.enabled),
5+
)
6+
37
const activeAgentDisplay = computed(() => displayActiveAgent(chatContext.activeAgent.value))
48
</script>
59

@@ -8,7 +12,7 @@ const activeAgentDisplay = computed(() => displayActiveAgent(chatContext.activeA
812
<Tooltip :delay-duration="500">
913
<TooltipTrigger as-child>
1014
<DropdownMenuTrigger as-child>
11-
<Button variant="ghost" size="sm" class="h-fit w-40 flex items-center justify-between gap-1 border-x border-primary border-opacity-80 px-2 py-1 -ml-1.5 light:border-primary-600 hover:bg-accent/30">
15+
<Button variant="ghost" size="sm" class="h-fit w-40 flex items-center justify-between gap-1 border-x-3px border-primary border-opacity-80 px-2 py-1 -ml-1.5 light:border-primary-600 hover:bg-accent/30">
1216
<div class="truncate">
1317
{{ activeAgentDisplay }}
1418
</div>
@@ -25,6 +29,18 @@ const activeAgentDisplay = computed(() => displayActiveAgent(chatContext.activeA
2529
>
2630
{{ model }}
2731
</DropdownMenuItem>
32+
<template v-for="[provider, providerSettings] of enabledProviders" :key="provider">
33+
<DropdownMenuSeparator />
34+
<DropdownMenuLabel>{{ $t(`chat.provider.${provider}`) }}</DropdownMenuLabel>
35+
<DropdownMenuSeparator />
36+
<DropdownMenuItem
37+
v-for="[model] of Object.entries(providerSettings.models).filter((([_m, v]) => v.enabled))"
38+
:key="model"
39+
@click="chatContext.agentsSetting.value.selectedAgent = `${provider}/${model}`"
40+
>
41+
{{ model }}
42+
</DropdownMenuItem>
43+
</template>
2844
</DropdownMenuContent>
2945
</TooltipTrigger>
3046
<TooltipContent side="bottom" :side-offset="6">

apps/frontend/app/components/chat/settings/GeneralSettingsSheet.vue

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,32 @@ import {
88
} from '@/lib/shadcn/components/ui/sheet'
99
import Input from '~/lib/shadcn/components/ui/input/Input.vue'
1010
import { useSidebar } from '~/lib/shadcn/components/ui/sidebar'
11+
import Switch from '~/lib/shadcn/components/ui/switch/Switch.vue'
1112
1213
const { $auth } = useNuxtApp()
1314
const sidebarContext = useSidebar()
15+
const { agentsSetting } = useChatContext()
1416
const { locale, locales, setLocale } = useI18n()
1517
const computedNextLocale = computed(() => {
1618
const currentLocaleIndex = locales.value.findIndex(lO => lO.code === locale.value)
1719
return locales.value[(currentLocaleIndex + 1) % locales.value.length]!.code
1820
})
1921
22+
// Providers that are supported through `Common` interface
23+
const supportedProvidersCommon = ['openrouter', 'openai'] as const
24+
25+
// Bootstraping object data for the supported providers
26+
for (const provider of supportedProvidersCommon) {
27+
if (!agentsSetting.value.providers[provider]) {
28+
agentsSetting.value.providers[provider] = {
29+
enabled: false,
30+
apiKey: '',
31+
models: {
32+
},
33+
}
34+
}
35+
}
36+
2037
const nicknameRef = useChatNickname()
2138
</script>
2239

@@ -70,11 +87,34 @@ const nicknameRef = useChatNickname()
7087
<hr>
7188

7289
<div>
73-
<!-- <SheetHeader>
90+
<SheetHeader class="mb-4">
7491
<SheetTitle class="text-base">
75-
{{ $t('chat.settings.general.title') }}
92+
{{ $t('chat.settings.providers.title') }}
7693
</SheetTitle>
77-
</SheetHeader> -->
94+
</SheetHeader>
95+
96+
<div
97+
v-for="[provider, setting] of supportedProvidersCommon.map((p) => [p, agentsSetting.providers[p]!] as const)"
98+
:key="provider"
99+
class="flex items-center justify-between gap-2"
100+
>
101+
<div class="shrink-0">
102+
{{ $t(`chat.provider.${provider}`) }}
103+
</div>
104+
105+
<div class="flex items-center gap-2">
106+
<Switch v-model="setting.enabled" :disabled="!Object.keys(setting.models).length">
107+
<template #thumb>
108+
<div class="i-hugeicons:zap" />
109+
</template>
110+
</Switch>
111+
<ProviderSettingsDialog :name="provider" :settings="setting">
112+
<Button variant="ghost" size="icon" class="group hover:bg-muted">
113+
<div class="i-hugeicons:configuration-01 size-6 group-hover:bg-mainGradient" />
114+
</Button>
115+
</ProviderSettingsDialog>
116+
</div>
117+
</div>
78118
</div>
79119
</div>
80120
</SheetContent>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<script setup lang="ts">
2+
import {
3+
Dialog,
4+
DialogContent,
5+
DialogDescription,
6+
DialogHeader,
7+
DialogTitle,
8+
DialogTrigger,
9+
} from '@/lib/shadcn/components/ui/dialog'
10+
import { Input } from '@/lib/shadcn/components/ui/input'
11+
import { Label } from '@/lib/shadcn/components/ui/label'
12+
import {
13+
TagsInput,
14+
TagsInputInput,
15+
TagsInputItem,
16+
TagsInputItemDelete,
17+
TagsInputItemText,
18+
} from '@/lib/shadcn/components/ui/tags-input'
19+
20+
const {
21+
name,
22+
settings,
23+
} = defineProps<{
24+
name: string
25+
settings: CommonProviderAgentsSetting
26+
}>()
27+
28+
const modelsRef = ref(Object.keys(settings.models))
29+
watch(modelsRef, () => {
30+
// eslint-disable-next-line vue/no-mutating-props
31+
settings.models = modelsRef.value.reduce((obj, m) => {
32+
obj[m] = { enabled: true }
33+
return obj
34+
}, {} as CommonProviderAgentsSetting['models'])
35+
})
36+
</script>
37+
38+
<template>
39+
<Dialog>
40+
<DialogTrigger as-child>
41+
<slot />
42+
</DialogTrigger>
43+
<DialogContent class="sm:max-w-[425px]">
44+
<DialogHeader>
45+
<DialogTitle>{{ $t(`chat.provider.${name}`) }}</DialogTitle>
46+
<DialogDescription>
47+
{{ $t('chat.settings.providerDialog.description') }}
48+
</DialogDescription>
49+
</DialogHeader>
50+
<div class="grid gap-4 py-4">
51+
<div class="grid items-center gap-1.5">
52+
<Label for="provider-settings-dialog_apiKey">{{ $t('chat.settings.providerDialog.form.apiKey') }}</Label>
53+
<!-- eslint-disable-next-line vue/no-mutating-props -->
54+
<Input id="provider-settings-dialog_apiKey" v-model="settings.apiKey" type="password" />
55+
</div>
56+
<div class="grid items-center gap-1.5">
57+
<Label for="provider-settings-dialog_apiKey">{{ $t('chat.settings.providerDialog.form.models') }}</Label>
58+
<!-- eslint-disable-next-line vue/no-mutating-props -->
59+
<TagsInput v-model="modelsRef">
60+
<TagsInputItem v-for="model in modelsRef" :key="model" :value="model">
61+
<TagsInputItemText />
62+
<TagsInputItemDelete />
63+
</TagsInputItem>
64+
65+
<TagsInputInput :placeholder="$t('form.tagsInput.placeholder')" />
66+
</TagsInput>
67+
</div>
68+
</div>
69+
</DialogContent>
70+
</Dialog>
71+
</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 { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'reka-ui'
3+
4+
const props = defineProps<DialogRootProps>()
5+
const emits = defineEmits<DialogRootEmits>()
6+
7+
const forwarded = useForwardPropsEmits(props, emits)
8+
</script>
9+
10+
<template>
11+
<DialogRoot v-bind="forwarded">
12+
<slot />
13+
</DialogRoot>
14+
</template>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script setup lang="ts">
2+
import { DialogClose, type DialogCloseProps } from 'reka-ui'
3+
4+
const props = defineProps<DialogCloseProps>()
5+
</script>
6+
7+
<template>
8+
<DialogClose v-bind="props">
9+
<slot />
10+
</DialogClose>
11+
</template>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { reactiveOmit } from '@vueuse/core'
4+
import { X } from 'lucide-vue-next'
5+
import {
6+
DialogClose,
7+
DialogContent,
8+
type DialogContentEmits,
9+
type DialogContentProps,
10+
DialogOverlay,
11+
DialogPortal,
12+
useForwardPropsEmits,
13+
} from 'reka-ui'
14+
import { cn } from '@/lib/shadcn/utils'
15+
16+
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
17+
const emits = defineEmits<DialogContentEmits>()
18+
19+
const delegatedProps = reactiveOmit(props, 'class')
20+
21+
const forwarded = useForwardPropsEmits(delegatedProps, emits)
22+
</script>
23+
24+
<template>
25+
<DialogPortal>
26+
<DialogOverlay
27+
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
28+
/>
29+
<DialogContent
30+
v-bind="forwarded"
31+
:class="
32+
cn(
33+
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
34+
props.class,
35+
)"
36+
>
37+
<slot />
38+
39+
<DialogClose
40+
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
41+
>
42+
<X class="w-4 h-4" />
43+
<span class="sr-only">Close</span>
44+
</DialogClose>
45+
</DialogContent>
46+
</DialogPortal>
47+
</template>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { reactiveOmit } from '@vueuse/core'
4+
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'reka-ui'
5+
import { cn } from '@/lib/shadcn/utils'
6+
7+
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
8+
9+
const delegatedProps = reactiveOmit(props, 'class')
10+
11+
const forwardedProps = useForwardProps(delegatedProps)
12+
</script>
13+
14+
<template>
15+
<DialogDescription
16+
v-bind="forwardedProps"
17+
:class="cn('text-sm text-muted-foreground', props.class)"
18+
>
19+
<slot />
20+
</DialogDescription>
21+
</template>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@/lib/shadcn/utils'
4+
5+
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
6+
</script>
7+
8+
<template>
9+
<div
10+
:class="
11+
cn(
12+
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
13+
props.class,
14+
)
15+
"
16+
>
17+
<slot />
18+
</div>
19+
</template>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@/lib/shadcn/utils'
4+
5+
const props = defineProps<{
6+
class?: HTMLAttributes['class']
7+
}>()
8+
</script>
9+
10+
<template>
11+
<div
12+
:class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)"
13+
>
14+
<slot />
15+
</div>
16+
</template>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { reactiveOmit } from '@vueuse/core'
4+
import { X } from 'lucide-vue-next'
5+
import {
6+
DialogClose,
7+
DialogContent,
8+
type DialogContentEmits,
9+
type DialogContentProps,
10+
DialogOverlay,
11+
DialogPortal,
12+
useForwardPropsEmits,
13+
} from 'reka-ui'
14+
import { cn } from '@/lib/shadcn/utils'
15+
16+
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
17+
const emits = defineEmits<DialogContentEmits>()
18+
19+
const delegatedProps = reactiveOmit(props, 'class')
20+
21+
const forwarded = useForwardPropsEmits(delegatedProps, emits)
22+
</script>
23+
24+
<template>
25+
<DialogPortal>
26+
<DialogOverlay
27+
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
28+
>
29+
<DialogContent
30+
:class="
31+
cn(
32+
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
33+
props.class,
34+
)
35+
"
36+
v-bind="forwarded"
37+
@pointer-down-outside="(event) => {
38+
const originalEvent = event.detail.originalEvent;
39+
const target = originalEvent.target as HTMLElement;
40+
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
41+
event.preventDefault();
42+
}
43+
}"
44+
>
45+
<slot />
46+
47+
<DialogClose
48+
class="absolute top-3 right-3 p-0.5 transition-colors rounded-md hover:bg-secondary"
49+
>
50+
<X class="w-4 h-4" />
51+
<span class="sr-only">Close</span>
52+
</DialogClose>
53+
</DialogContent>
54+
</DialogOverlay>
55+
</DialogPortal>
56+
</template>

0 commit comments

Comments
 (0)