feat: keyboard shortcuts to send message in a conversations (CTRL + Enter) to send, enter for newline.

- feat: button to show/hide quoted text of incoming emails.
- fix: logout redirect
- remove hardcoded user login info.
- add libredesk.io to webtemplate footer
This commit is contained in:
Abhinav Raut
2025-01-10 02:45:55 +05:30
parent 1ca40d57df
commit f30a7f07d0
15 changed files with 216 additions and 185 deletions

View File

@@ -193,13 +193,8 @@ func handleGetTeamUnassignedConversations(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid `team_id`", nil, envelope.InputError)
}
user, err := app.user.Get(auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Check if user belongs to the team.
exists, err := app.team.UserBelongsToTeam(teamID, user.ID)
exists, err := app.team.UserBelongsToTeam(teamID, auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}

View File

@@ -50,5 +50,5 @@ func handleLogout(r *fastglue.Request) error {
"no-store, no-cache, must-revalidate, post-check=0, pre-check=0")
r.RequestCtx.Response.Header.Add("Pragma", "no-cache")
r.RequestCtx.Response.Header.Add("Expires", "-1")
return r.RedirectURI("dashboard", fasthttp.StatusFound, nil, "")
return r.RedirectURI("/", fasthttp.StatusFound, nil, "")
}

View File

@@ -2,6 +2,7 @@ package main
import (
"net/http"
"strings"
amodels "github.com/abhinavxd/artemis/internal/auth/models"
"github.com/abhinavxd/artemis/internal/envelope"
@@ -73,49 +74,49 @@ func auth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
func perm(handler fastglue.FastRequestHandler, perm string) fastglue.FastRequestHandler {
return func(r *fastglue.Request) error {
var (
// app = r.Context.(*App)
// cookieToken = string(r.RequestCtx.Request.Header.Cookie("csrf_token"))
// hdrToken = string(r.RequestCtx.Request.Header.Peek("X-CSRFTOKEN"))
app = r.Context.(*App)
cookieToken = string(r.RequestCtx.Request.Header.Cookie("csrf_token"))
hdrToken = string(r.RequestCtx.Request.Header.Peek("X-CSRFTOKEN"))
)
// if cookieToken == "" || hdrToken == "" || cookieToken != hdrToken {
// app.lo.Error("csrf token mismatch", "cookie_token", cookieToken, "header_token", hdrToken)
// return r.SendErrorEnvelope(http.StatusForbidden, "Invalid CSRF token", nil, envelope.PermissionError)
// }
if cookieToken == "" || hdrToken == "" || cookieToken != hdrToken {
app.lo.Error("csrf token mismatch", "cookie_token", cookieToken, "header_token", hdrToken)
return r.SendErrorEnvelope(http.StatusForbidden, "Invalid CSRF token", nil, envelope.PermissionError)
}
// Validate session and fetch user.
// sessUser, err := app.auth.ValidateSession(r)
// if err != nil || sessUser.ID <= 0 {
// app.lo.Error("error validating session", "error", err)
// return r.SendErrorEnvelope(http.StatusUnauthorized, "Invalid or expired session", nil, envelope.PermissionError)
// }
sessUser, err := app.auth.ValidateSession(r)
if err != nil || sessUser.ID <= 0 {
app.lo.Error("error validating session", "error", err)
return r.SendErrorEnvelope(http.StatusUnauthorized, "Invalid or expired session", nil, envelope.PermissionError)
}
// // Get user from DB.
// user, err := app.user.Get(sessUser.ID)
// if err != nil {
// return sendErrorEnvelope(r, err)
// }
// Get user from DB.
user, err := app.user.Get(sessUser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Split the permission string into object and action and enforce it.
parts := strings.Split(perm, ":")
if len(parts) != 2 {
return r.SendErrorEnvelope(http.StatusInternalServerError, "Invalid permission format", nil, envelope.GeneralError)
}
object, action := parts[0], parts[1]
ok, err := app.authz.Enforce(user, object, action)
if err != nil {
return r.SendErrorEnvelope(http.StatusInternalServerError, "Error checking permissions", nil, envelope.GeneralError)
}
if !ok {
return r.SendErrorEnvelope(http.StatusForbidden, "Permission denied", nil, envelope.PermissionError)
}
// // Split the permission string into object and action and enforce it.
// parts := strings.Split(perm, ":")
// if len(parts) != 2 {
// return r.SendErrorEnvelope(http.StatusInternalServerError, "Invalid permission format", nil, envelope.GeneralError)
// }
// object, action := parts[0], parts[1]
// ok, err := app.authz.Enforce(user, object, action)
// if err != nil {
// return r.SendErrorEnvelope(http.StatusInternalServerError, "Error checking permissions", nil, envelope.GeneralError)
// }
// if !ok {
// return r.SendErrorEnvelope(http.StatusForbidden, "Permission denied", nil, envelope.PermissionError)
// }
// Set user in the request context.
r.RequestCtx.SetUserValue("user", amodels.User{
ID: 1,
Email: "sample@example.com",
FirstName: "Sample",
LastName: "User",
ID: user.ID,
Email: user.Email.String,
FirstName: user.FirstName,
LastName: user.LastName,
})
return handler(r)

View File

@@ -287,11 +287,7 @@ func handleGetCurrentUser(r *fastglue.Request) error {
app = r.Context.(*App)
auser = r.RequestCtx.UserValue("user").(amodels.User)
)
user, err := app.user.Get(auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
u, err := app.user.Get(user.ID)
u, err := app.user.Get(auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}

View File

@@ -20,6 +20,7 @@
"@radix-icons/vue": "^1.0.0",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/vue-table": "^8.19.2",
"@tiptap/extension-hard-break": "^2.11.0",
"@tiptap/extension-image": "^2.5.9",
"@tiptap/extension-link": "^2.9.1",
"@tiptap/extension-list-item": "^2.4.0",

View File

@@ -12,11 +12,14 @@
padding: 1rem 1rem;
height: 100%;
overflow-y: scroll;
padding-bottom: 50px;
padding-bottom: 100px;
}
body {
overflow: hidden;
}
// Theme.
@layer base {
:root {
--background: 0 0% 100%;
@@ -145,7 +148,6 @@
border
rounded-xl;
box-shadow: 1px 1px 1px 0px rgba(0, 0, 0, 0.1);
// To make email tables fit.
table {
width: 100% !important;
}
@@ -266,3 +268,15 @@ a[data-active="false"]:hover {
font-weight: 500;
transition: background-color 0.2s, color 0.2s;
}
.show-quoted-text {
blockquote {
@apply block;
}
}
.hide-quoted-text {
blockquote {
@apply hidden;
}
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="overflow-y-auto ">
<div>
<slot></slot>
</div>
</template>

View File

@@ -1,6 +1,7 @@
<template>
<div class="max-h-[600px] overflow-y-auto">
<BubbleMenu :editor="editor" :tippy-options="{ duration: 100 }" v-if="editor" class="bg-white p-1 box rounded-lg">
<BubbleMenu :editor="editor" :tippy-options="{ duration: 100 }" v-if="editor"
class="bg-white p-1 box rounded-lg will-change-transform">
<div class="flex space-x-1 items-center">
<DropdownMenu>
<DropdownMenuTrigger>
@@ -44,6 +45,7 @@ import {
} from '@/components/ui/dropdown-menu'
import Placeholder from '@tiptap/extension-placeholder'
import Image from '@tiptap/extension-image'
import HardBreak from '@tiptap/extension-hard-break'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
@@ -52,9 +54,7 @@ const textContent = defineModel('textContent')
const htmlContent = defineModel('htmlContent')
const isBold = defineModel('isBold')
const isItalic = defineModel('isItalic')
const cursorPosition = defineModel('cursorPosition', {
default: 0
})
const cursorPosition = defineModel('cursorPosition', { default: 0 })
const props = defineProps({
placeholder: String,
@@ -66,36 +66,52 @@ const props = defineProps({
}
})
const emit = defineEmits([
'send',
'editorReady',
'aiPromptSelected'
])
const emit = defineEmits(['send', 'editorReady', 'aiPromptSelected'])
function emitPrompt (key) {
emit('aiPromptSelected', key)
const emitPrompt = (key) => emit('aiPromptSelected', key)
const getSelectionText = (from, to, doc) => doc.textBetween(from, to)
const editorConfig = {
extensions: [
StarterKit.configure({
hardBreak: false,
}),
HardBreak.extend({
addKeyboardShortcuts () {
return {
Enter: () => {
if (this.editor.isActive('orderedList') || this.editor.isActive('bulletList')) {
return this.editor.chain().createParagraphNear().run();
}
return this.editor.commands.setHardBreak();
},
}
}
}),
Image.configure({ HTMLAttributes: { class: 'inline-image' } }),
Placeholder.configure({ placeholder: () => props.placeholder }),
Link
],
autofocus: true,
editorProps: {
attributes: { class: 'outline-none' },
handleKeyDown: (view, event) => {
if (event.ctrlKey && event.key === 'Enter') {
emit('send')
return true
}
}
}
}
const editor = ref(
useEditor({
...editorConfig,
content: textContent.value,
extensions: [
StarterKit,
Image.configure({
HTMLAttributes: { class: 'inline-image' }
}),
Placeholder.configure({
placeholder: () => props.placeholder
}),
Link
],
autofocus: true,
editorProps: { attributes: { class: 'outline-none' } },
onSelectionUpdate: ({ editor }) => {
selectedText.value = editor.state.doc.textBetween(
editor.state.selection.from,
editor.state.selection.to
)
const { from, to } = editor.state.selection
selectedText.value = getSelectionText(from, to, editor.state.doc)
},
onUpdate: ({ editor }) => {
htmlContent.value = editor.getHTML()
@@ -106,41 +122,44 @@ const editor = ref(
if (cursorPosition.value) {
editor.commands.setTextSelection(cursorPosition.value)
}
},
}
})
)
watchEffect(() => {
if (editor.value) {
emit('editorReady', editor.value)
isBold.value = editor.value.isActive('bold')
isItalic.value = editor.value.isActive('italic')
}
const editorInstance = editor.value
if (!editorInstance) return
emit('editorReady', editorInstance)
isBold.value = editorInstance.isActive('bold')
isItalic.value = editorInstance.isActive('italic')
})
watchEffect(() => {
if (isBold.value !== editor.value?.isActive('bold')) {
const editorInstance = editor.value
if (!editorInstance) return
if (isBold.value !== editorInstance.isActive('bold')) {
isBold.value
? editor.value?.chain().focus().setBold().run()
: editor.value?.chain().focus().unsetBold().run()
? editorInstance.chain().focus().setBold().run()
: editorInstance.chain().focus().unsetBold().run()
}
if (isItalic.value !== editor.value?.isActive('italic')) {
if (isItalic.value !== editorInstance.isActive('italic')) {
isItalic.value
? editor.value?.chain().focus().setItalic().run()
: editor.value?.chain().focus().unsetItalic().run()
? editorInstance.chain().focus().setItalic().run()
: editorInstance.chain().focus().unsetItalic().run()
}
})
watch(
() => props.contentToSet,
(newContent) => {
if (newContent) {
console.log('Setting content to -:', newContent)
editor.value.commands.setContent(newContent)
editor.value.commands.focus()
}
watch(() => props.contentToSet, (newContent) => {
console.log('newContent', newContent)
if (newContent === '') {
editor.value?.commands.clearContent()
} else {
editor.value?.commands.setContent(newContent, true)
}
)
editor.value?.commands.focus()
})
watch(cursorPosition, (newPos, oldPos) => {
if (editor.value && newPos !== oldPos && newPos !== editor.value.state.selection.from) {
@@ -149,7 +168,7 @@ watch(cursorPosition, (newPos, oldPos) => {
})
onUnmounted(() => {
editor.value.destroy()
editor.value?.destroy()
})
</script>

View File

@@ -1,6 +1,7 @@
<template>
<div>
<!-- Fullscreen editor -->
<!-- TODO: Has to be a better way to do this than creating two separate editor components -->
<Dialog :open="isEditorFullscreen" @update:open="isEditorFullscreen = $event">
<DialogContent class="max-w-[70%] max-h-[70%] h-[70%] m-0 p-6">
<div v-if="isEditorFullscreen">
@@ -19,8 +20,7 @@
<Editor v-model:selectedText="selectedText" v-model:isBold="isBold" v-model:isItalic="isItalic"
v-model:htmlContent="htmlContent" v-model:textContent="textContent" :placeholder="editorPlaceholder"
:aiPrompts="aiPrompts" @keydown="handleKeydown" @aiPromptSelected="handleAiPromptSelected"
@editorReady="onEditorReady" @clearContent="clearContent" :contentToSet="contentToSet"
v-model:cursorPosition="cursorPosition" />
@editorReady="onEditorReady" :contentToSet="contentToSet" v-model:cursorPosition="cursorPosition" />
</div>
</DialogContent>
</Dialog>
@@ -58,7 +58,7 @@
<Editor v-model:selectedText="selectedText" v-model:isBold="isBold" v-model:isItalic="isItalic"
v-model:htmlContent="htmlContent" v-model:textContent="textContent" :placeholder="editorPlaceholder"
:aiPrompts="aiPrompts" @keydown="handleKeydown" @aiPromptSelected="handleAiPromptSelected"
@editorReady="onEditorReady" @clearContent="clearContent" :contentToSet="contentToSet"
@editorReady="onEditorReady" :contentToSet="contentToSet" @send="handleSend"
v-model:cursorPosition="cursorPosition" />
<!-- Attachments preview -->
@@ -101,7 +101,6 @@ const cursorPosition = ref(0)
const selectedText = ref('')
const htmlContent = ref('')
const textContent = ref('')
const clearContent = ref(false)
const contentToSet = ref('')
const isBold = ref(false)
const isItalic = ref(false)
@@ -116,6 +115,8 @@ const cannedResponses = ref([])
const aiPrompts = ref([])
const editorPlaceholder = "Press Enter to add a new line; Press '/' to select a Canned Response; Press Ctrl + Enter to send."
onMounted(async () => {
await Promise.all([fetchCannedResponses(), fetchAiPrompts()])
})
@@ -170,10 +171,6 @@ const toggleItalic = () => {
isItalic.value = !isItalic.value
}
const editorPlaceholder = computed(() => {
return "Press enter to add a new line; Press '/' to select a Canned Response."
})
const attachments = computed(() => {
return uploadedFiles.value.filter(upload => upload.disposition === 'attachment')
})
@@ -287,10 +284,9 @@ const handleSend = async () => {
variant: 'destructive',
description: handleHTTPError(error).message
})
} finally {
clearContent.value = true
uploadedFiles.value = []
}
editorInstance.value.commands.clearContent()
uploadedFiles.value = []
api.updateAssigneeLastSeen(conversationStore.current.uuid)
}

View File

@@ -12,9 +12,17 @@
{{ avatarFallback }}
</AvatarFallback>
</Avatar>
<div class="flex flex-col justify-end message-bubble !rounded-tl-none contact-message-bubble">
<Letter :html="messageContent" :allowedSchemas="['cid', 'https', 'http']" class="mb-1"
<div class="flex flex-col justify-end message-bubble !rounded-tl-none" :class="{
'show-quoted-text': showQuotedText,
'hide-quoted-text': !showQuotedText
}">
<Letter :html="sanitizedMessageContent" :allowedSchemas="['cid', 'https', 'http']" class="mb-1"
:class="{ 'mb-3': message.attachments.length > 0 }" />
<div v-if="hasQuotedContent" @click="toggleQuote"
class="text-xs cursor-pointer text-muted-foreground px-2 py-1 w-max hover:bg-muted hover:text-primary rounded-md transition-all">
{{ showQuotedText ? 'Hide quoted text' : 'Show quoted text' }}
</div>
<MessageAttachmentPreview :attachments="nonInlineAttachments" />
</div>
</div>
@@ -36,10 +44,10 @@
</template>
<script setup>
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { format } from 'date-fns'
import { useConversationStore } from '@/stores/conversation'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Letter } from 'vue-letter'
@@ -48,28 +56,39 @@ import MessageAttachmentPreview from '@/components/attachment/MessageAttachmentP
const props = defineProps({
message: Object
})
const convStore = useConversationStore()
const showQuotedText = ref(false)
const getAvatar = computed(() => {
return convStore.current.avatar_url ? convStore.conversation.avatar_url : ''
return convStore.current.avatar_url || ''
})
const messageContent = computed(() =>
props.message.attachments.reduce((content, { content_id, url }) =>
content.replace(new RegExp(`cid:${content_id}`, 'g'), url),
props.message.content
const sanitizedMessageContent = computed(() => {
const content = props.message.content || ''
return props.message.attachments.reduce((acc, { content_id, url }) =>
acc.replace(new RegExp(`cid:${content_id}`, 'g'), url),
content
)
)
})
const nonInlineAttachments = computed(() =>
const hasQuotedContent = computed(() => sanitizedMessageContent.value.includes('<blockquote'))
const toggleQuote = () => {
showQuotedText.value = !showQuotedText.value
}
const nonInlineAttachments = computed(() =>
props.message.attachments.filter(attachment => attachment.disposition !== 'inline')
)
const getFullName = computed(() => {
return convStore.current.contact.first_name + ' ' + convStore.current.contact.last_name
const contact = convStore.current.contact || {}
return `${contact.first_name || ''} ${contact.last_name || ''}`.trim()
})
const avatarFallback = computed(() => {
return convStore.current.contact.first_name.toUpperCase().substring(0, 2)
const contact = convStore.current.contact || {}
return (contact.first_name || '').toUpperCase().substring(0, 2)
})
</script>

View File

@@ -4,42 +4,35 @@
<SidebarMenuButton size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground p-0">
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage src="https://avatars.githubusercontent.com/u/48166553?v=4" alt="Abhinav" />
<AvatarImage :src="userStore.avatar" alt="Abhinav" />
<AvatarFallback class="rounded-lg">
AR
{{ userStore.getInitials }}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">Abhinav Raut</span>
<span class="truncate text-xs">abhinavrautcs@gmail.com</span>
<span class="truncate font-semibold">{{ userStore.getFullName }}</span>
<span class="truncate text-xs">{{ userStore.email }}</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom"
align="end" :side-offset="4">
:side-offset="4">
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage src="https://avatars.githubusercontent.com/u/48166553?v=4" alt="Abhinav" />
<AvatarImage :src="userStore.avatar" alt="Abhinav" />
<AvatarFallback class="rounded-lg">
AR
{{ userStore.getInitials }}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">Abhinav Raut!</span>
<span class="truncate text-xs">abhinavrautcs@gmail.com</span>
<span class="truncate font-semibold">{{ userStore.getFullName }}</span>
<span class="truncate text-xs">{{ userStore.email }}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<!-- <DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles size="18" class="mr-2" />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator /> -->
<DropdownMenuGroup>
<DropdownMenuItem>
<router-link to="/account" class="flex items-center">
@@ -47,19 +40,14 @@
Account
</router-link>
</DropdownMenuItem>
<!-- <DropdownMenuItem>
<Bell size="18" class="mr-2" />
Notifications
</DropdownMenuItem> -->
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem @click.stop="window.location.href = '/logout'">
<DropdownMenuItem @click="logout">
<LogOut size="18" class="mr-2" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
<script setup>
@@ -81,37 +69,14 @@ import {
AvatarImage,
} from '@/components/ui/avatar'
import {
AudioWaveform,
BadgeCheck,
Bell,
Users,
CircleUserRound,
Bot,
Inbox,
ChevronRight,
ChevronsUpDown,
Command,
CreditCard,
SlidersHorizontal,
Folder,
Shield,
FileLineChart,
Forward,
EllipsisVertical,
MessageCircleHeart,
Plus,
BarChart,
MessageCircle,
Frame,
GalleryVerticalEnd,
CircleUserRound,
LogOut,
Map,
MoreHorizontal,
PieChart,
Settings2,
Sparkles,
ListFilter,
SquareTerminal,
Trash2,
} from 'lucide-vue-next'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const logout = () => {
window.location.href = '/logout'
}
</script>

View File

@@ -2,6 +2,9 @@
<div class="page-content">
<Spinner v-if="isLoading"></Spinner>
<div class="space-y-4">
<div class="text-sm text-gray-500 text-right">
Last updated: {{ new Date(lastUpdate).toLocaleTimeString() }}
</div>
<div class="mt-7 flex w-full space-x-4" v-auto-animate>
<Card title="Open conversations" :counts="cardCounts" :labels="agentCountCardsLabels" />
<Card class="w-8/12" title="Agent status" :counts="sampleAgentStatusCounts" :labels="sampleAgentStatusLabels" />
@@ -17,43 +20,61 @@
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { useToast } from '@/components/ui/toast/use-toast'
import api from '@/api'
import { vAutoAnimate } from '@formkit/auto-animate/vue'
import Card from '@/components/dashboard/DashboardCard.vue'
import LineChart from '@/components/dashboard/DashboardLineChart.vue'
import BarChart from '@/components/dashboard/DashboardBarChart.vue'
import Spinner from '@/components/ui/spinner/Spinner.vue'
const { toast } = useToast()
const isLoading = ref(false)
const cardCounts = ref({})
const chartData = ref({})
const lastUpdate = ref(new Date())
let updateInterval
const agentCountCardsLabels = {
open: 'Total',
awaiting_response: 'Awaiting Response',
unassigned: 'Unassigned',
pending: 'Pending'
}
// TODO: Build agent status feature.
const sampleAgentStatusLabels = {
online: 'Online',
offline: 'Offline',
away: 'Away',
away: 'Away'
}
const sampleAgentStatusCounts = {
online: 5,
offline: 2,
away: 1,
away: 1
}
onMounted(() => {
getDashboardData()
startRealtimeUpdates()
})
onUnmounted(() => {
stopRealtimeUpdates()
})
const startRealtimeUpdates = () => {
updateInterval = setInterval(() => {
getDashboardData()
lastUpdate.value = new Date()
}, 60000)
}
const stopRealtimeUpdates = () => {
clearInterval(updateInterval)
}
const getDashboardData = () => {
isLoading.value = true
Promise.all([getCardStats(), getDashboardCharts()])

View File

@@ -1380,6 +1380,11 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.4.0.tgz#2a738509d40f5f856492c11e32b10e4462f71216"
integrity sha512-F4y/0J2lseohkFUw9P2OpKhrJ6dHz69ZScABUvcHxjznJLd6+0Zt7014Lw5PA8/m2d/w0fX8LZQ88pZr4quZPQ==
"@tiptap/extension-hard-break@^2.11.0":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.11.0.tgz#0e199a1c58171fe1879e730b87666e759fe55c04"
integrity sha512-7pMgPNk2FnPT0LcWaWNNxOLK3LQnRSYFgrdBGMXec3sy+y3Lit3hM+EZhbZcHpTIQTbWWs+eskh1waRMIt0ZaQ==
"@tiptap/extension-hard-break@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.4.0.tgz#b5bf5b065827280e450fba8f53d137654509d836"

View File

@@ -1,4 +1,4 @@
// Package ai handles management of LLM providers.
// Package ai manages AI prompts and integrates with LLM providers.
package ai
import (
@@ -51,7 +51,6 @@ func New(opts Opts) (*Manager, error) {
// SendPrompt sends a prompt to the default provider and returns the response.
func (m *Manager) SendPrompt(k string, prompt string) (string, error) {
// return "Hello Abhinav,\n\nI wanted to let you know that we have successfully added a top-up of Rs 20,000 to your account. You can expect to receive the funds by the end of the day.\n\nIf you have any concerns or questions, please don't hesitate to update this ticket. We are always happy to assist you in any way we can.\n\nPlease keep in mind that if we do not hear back from you within 24 hours, the ticket will be marked as <i>closed</i>. However, you can easily reopen it if you need further assistance. Additionally, you can always reach out to us through our support portal for any help you may need.\n\nThank you for choosing our services. We are here to make your experience as smooth and hassle-free as possible.\n\nBest regards, \n[Your Name]", nil
systemPrompt, err := m.getPrompt(k)
if err != nil {
return "", err

View File

@@ -35,7 +35,7 @@
</div>
<footer class="container">
Powered by <a target="_blank" rel="noreferrer" href="#">Artemis</a>
Powered by <a target="_blank" rel="noreferrer" href="https://libredesk.io/">LibreDesk</a>
</footer>
</body>
</html>