GitHub

@@ -0,0 +1,132 @@

1+

<script setup lang="ts">

2+

import { Extension } from '@tiptap/core'

3+

import type { EditorMentionMenuItem, SelectItem } from '@nuxt/ui'

4+5+

const input = ref('')

6+

const mode = ref('auto')

7+

const attachments = ref<{ name: string, src?: string }[]>([])

8+

const fileInputRef = useTemplateRef('fileInputRef')

9+10+

const files: EditorMentionMenuItem[] = [

11+

{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },

12+

{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' },

13+

{ label: 'package.json', icon: 'i-vscode-icons-file-type-json' },

14+

{ label: 'README.md', icon: 'i-vscode-icons-file-type-markdown' },

15+

{ label: 'AuthForm.vue', icon: 'i-vscode-icons-file-type-vue' },

16+

{ label: 'useChat.ts', icon: 'i-vscode-icons-file-type-typescript' }

17+

]

18+19+

const commands: EditorMentionMenuItem[] = [

20+

{ label: 'init', icon: 'i-lucide-sparkles' },

21+

{ label: 'review', icon: 'i-lucide-search-code' },

22+

{ label: 'security-review', icon: 'i-lucide-shield-check' },

23+

{ label: 'clear', icon: 'i-lucide-eraser' },

24+

{ label: 'compact', icon: 'i-lucide-fold-vertical' },

25+

{ label: 'config', icon: 'i-lucide-settings' }

26+

]

27+28+

const modes = [

29+

{ label: 'Manual', value: 'manual', icon: 'i-lucide-hand' },

30+

{ label: 'Edit automatically', value: 'edit', icon: 'i-lucide-code-xml' },

31+

{ label: 'Plan mode', value: 'plan', icon: 'i-lucide-list-checks' },

32+

{ label: 'Auto mode', value: 'auto', icon: 'i-lucide-zap' }

33+

] satisfies SelectItem[]

34+35+

// SSR-safe target so the menus aren't clipped by overflow

36+

const appendToBody = import.meta.client ? () => document.body : undefined

37+38+

function onFilesChange(event: Event) {

39+

const target = event.target as HTMLInputElement

40+

for (const file of Array.from(target.files ?? [])) {

41+

attachments.value.push({

42+

name: file.name,

43+

src: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined

44+

})

45+

}

46+

target.value = ''

47+

}

48+49+

function onSubmit() {

50+

console.log('submit', input.value)

51+52+

input.value = ''

53+

attachments.value = []

54+

}

55+

</script>

56+57+

<template>

58+

<UChatPrompt

59+

v-model="input"

60+

class="w-full p-0 gap-0"

61+

placeholder="Press / to open the command menu"

62+

:ui="{

63+

header: 'px-2.5 py-2 border-b border-default',

64+

footer: 'px-2.5 py-2 border-t border-default'

65+

}"

66+

@submit="onSubmit"

67+

>

68+

<template v-if="attachments.length" #header>

69+

<UButton

70+

v-for="(file, index) in attachments"

71+

:key="index"

72+

:label="file.name"

73+

:avatar="file.src ? { src: file.src } : undefined"

74+

:icon="file.src ? undefined : 'i-lucide-file'"

75+

color="neutral"

76+

variant="soft"

77+

size="xs"

78+

square

79+

trailing-icon="i-lucide-x"

80+

@click="attachments.splice(index, 1)"

81+

/>

82+

</template>

83+84+

<template #body="{ submit, placeholder }">

85+

<UEditor

86+

v-slot="{ editor }"

87+

v-model="input"

88+

content-type="markdown"

89+

:starter-kit="false"

90+

:placeholder="placeholder"

91+

class="w-full min-h-12"

92+

:ui="{ base: 'p-2.5! [&_.mention]:bg-primary/5 [&_.mention]:rounded-sm [&_.mention]:px-0.5 [&_.mention]:py-0.25' }"

93+

:extensions="[Extension.create({

94+

name: 'chatPromptSubmit',

95+

priority: 1000,

96+

addKeyboardShortcuts: () => ({ Enter: () => (submit(), true) })

97+

})]"

98+

>

99+

<UEditorMentionMenu :editor="editor" char="@" plugin-key="mention" :items="files" :append-to="appendToBody" />

100+

<UEditorMentionMenu :editor="editor" char="/" plugin-key="command" :items="commands" :append-to="appendToBody" />

101+

</UEditor>

102+

</template>

103+104+

<template #footer>

105+

<div class="flex items-center gap-0.5">

106+

<UButton

107+

icon="i-lucide-plus"

108+

color="neutral"

109+

variant="ghost"

110+

aria-label="Attach files"

111+

size="sm"

112+

@click="fileInputRef?.click()"

113+

/>

114+

<input ref="fileInputRef" type="file" multiple class="hidden" @change="onFilesChange">

115+

</div>

116+117+

<div class="flex items-center gap-1">

118+

<USelect

119+

v-model="mode"

120+

:items="modes"

121+

:icon="modes.find(item => item.value === mode)?.icon"

122+

color="neutral"

123+

variant="ghost"

124+

size="sm"

125+

square

126+

/>

127+128+

<UChatPromptSubmit size="sm" :disabled="!input.trim()" />

129+

</div>

130+

</template>

131+

</UChatPrompt>

132+

</template>

Read the original on github.com ↗