11import { ref , nextTick , onMounted , watch } from 'vue'
2- import { renderMarkdown } from "../../lib/mjs/markdown.mjs"
2+ import { renderMarkdown , htmlEncode } from "../../lib/mjs/markdown.mjs"
33
44const collection = 'typesense_docs'
55const BaseUrl = "https://search.docs.servicestack.net" ;
@@ -65,6 +65,7 @@ async function multiSearch(message, conversationId = null) {
6565 exclude_fields : "embedding"
6666 } ]
6767 } )
68+
6869 const result = response . results [ 0 ]
6970 const { answer, conversation_id, query } = response . conversation
7071 const { found, out_of, page, request_params, search_cutoff, search_time_ms } = result
@@ -80,6 +81,8 @@ async function multiSearch(message, conversationId = null) {
8081 search_time_ms,
8182 results : response . results . length ,
8283 hits : result . hits . map ( ( x ) => ( {
84+ id : x . document . id ,
85+ objectID : x . document . objectID ,
8386 url : x . document . url ,
8487 anchor : x . document . anchor ,
8588 content : clean ( x . document . content ) ,
@@ -94,21 +97,40 @@ async function multiSearch(message, conversationId = null) {
9497}
9598
9699const AISearchDialog = {
97- template : `<div v-if="open" class="search-dialog fixed inset-0 z-50 flex bg-black/25 items-center justify-center" @click="$emit('hide')">
100+ template : `<div v-if="open" class="search-dialog fixed inset-0 z-50 flex bg-black/25 items-center justify-center" @click="$emit('hide')" @keydown.escape="$emit('hide')" >
98101 <div class="dialog absolute w-full max-w-2xl flex flex-col bg-indigo-50 dark:bg-indigo-900 rounded-lg shadow-lg" style="max-height:80vh;" @click.stop="">
99102 <div class="p-4 flex flex-col" style="max-height: 80vh;">
100103 <!-- Header -->
101104 <div class="flex items-center justify-between mb-4 bg-indigo-900 dark:bg-indigo-950 p-4 -m-4 mb-4 rounded-t-lg">
102- <h2 class="text-xl font-semibold text-white">Ask ServiceStack Docs</h2>
103- <button type="button" @click="$emit('hide')" class="text-gray-400 hover:text-white dark:hover:text-white">
104- <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
105- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
106- </svg>
107- </button>
105+ <div class="flex items-center gap-2">
106+ <h2 class="text-xl font-semibold text-white">Ask ServiceStack Docs</h2>
107+ </div>
108+ <div class="flex space-x-3">
109+ <button type="button" v-if="messages.length > 0" @click="clearConversation"
110+ class="text-xs text-gray-300 hover:text-white dark:hover:text-white font-semibold">
111+ <svg class="inline-block size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M14 11v6m-4-6v6M6 7v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7M4 7h16M7 7l2-4h6l2 4" stroke-width="1"/></svg>
112+ clear
113+ </button>
114+ <button type="button" @click="$emit('hide')" class="text-gray-400 hover:text-white dark:hover:text-white">
115+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
116+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
117+ </svg>
118+ </button>
119+ </div>
120+ </div>
121+
122+ <!-- Suggestions -->
123+ <div v-if="messages.length === 0" class="mb-4">
124+ <p class="text-xs text-gray-600 dark:text-gray-400 font-semibold mb-2">Suggestions:</p>
125+ <div class="flex flex-wrap gap-2">
126+ <button v-for="suggestion in suggestions" :key="suggestion" type="button" @click="inputMessage = suggestion; sendMessage()" class="text-xs px-3 py-1 bg-blue-100 dark:bg-blue-900 border border-blue-400 dark:border-blue-600 text-blue-700 dark:text-blue-200 hover:bg-blue-200 dark:hover:bg-blue-800 rounded-full transition-colors">
127+ {{ suggestion }}
128+ </button>
129+ </div>
108130 </div>
109131
110132 <!-- Messages Area -->
111- <div class="flex-1 overflow-y-auto mb-4 pr-2 space-y-4" style="max-height: calc(80vh - 180px);">
133+ <div class="flex-1 overflow-y-auto mb-4 pr-2 space-y-4 border-b border-gray-300 dark:border-gray-600 " style="max-height: calc(80vh - 180px);">
112134 <div v-for="(msg, idx) in messages" :key="idx" :class="['message', msg.role === 'user' ? 'user-message' : 'assistant-message']">
113135 <div v-if="msg.role === 'user'" class="flex justify-end">
114136 <div class="bg-indigo-900 dark:bg-indigo-950 text-white rounded-lg px-4 py-2 max-w-xs">
@@ -117,14 +139,11 @@ const AISearchDialog = {
117139 </div>
118140 <div v-else class="flex flex-col">
119141 <div class="shadow prose bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white rounded-lg px-4 py-2 mb-3"
120- v-html="renderMarkdown (msg.content )"></div>
142+ v-html="renderContent (msg)"></div>
121143 <!-- Search Results -->
122144 <div v-if="getUniqueHits(msg.hits).length > 0" class="space-y-3 mt-3">
123145 <div class="flex items-center justify-between gap-2">
124146 <p class="text-sm text-gray-600 dark:text-gray-400 font-semibold">{{ getUniqueHits(msg.hits).length }} Result{{ getUniqueHits(msg.hits).length !== 1 ? 's' : '' }} Found</p>
125- <button type="button" @click="clearConversation" class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 hover:underline font-semibold">
126- clear
127- </button>
128147 </div>
129148 <a v-for="(hit, hitIdx) in getUniqueHits(msg.hits)" :key="hitIdx" :href="hit.url" :class="[hit.type ===
130149 'lvl0' || hit.type === 'lvl1' ?
@@ -141,22 +160,16 @@ const AISearchDialog = {
141160 {{ hit.content }}
142161 </p>
143162 </a>
144- <!-- Clear Button After Results -->
145- <div class="flex justify-center pt-2">
146- <button type="button" @click="clearConversation" class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 hover:underline font-semibold">
147- clear
148- </button>
149- </div>
150163 </div>
151164 </div>
152165 </div>
153- <div v-if="loading" class="flex justify-center min-h-10">
166+ <div v-if="loading" class="mb-2 flex justify-center min-h-10">
154167 <div class="absolute animate-spin rounded-full size-8 border-b-2 border-blue-500 dark:border-blue-400"></div>
155168 </div>
156169 </div>
157170
158171 <!-- Input Area -->
159- <div class="flex gap-2 pt-4 ">
172+ <div class="flex gap-2">
160173 <input ref="refMessage"
161174 v-model="inputMessage"
162175 @keyup.enter="sendMessage"
@@ -184,6 +197,11 @@ const AISearchDialog = {
184197 const inputMessage = ref ( '' )
185198 const loading = ref ( false )
186199 const conversationId = ref ( null )
200+ const suggestions = [
201+ "What is AutoQuery?" ,
202+ "How do I Authenticate?" ,
203+ "What Add ServiceStack Reference languages are supported?"
204+ ]
187205
188206 function getUniqueHits ( hits ) {
189207 if ( ! hits ) return [ ]
@@ -234,7 +252,12 @@ const AISearchDialog = {
234252 nextTick ( ( ) => {
235253 const messagesArea = document . querySelector ( '.search-dialog .overflow-y-auto' )
236254 if ( messagesArea ) {
237- messagesArea . scrollTop = messagesArea . scrollHeight
255+ // Scroll to the user message (second to last message)
256+ const messages = messagesArea . querySelectorAll ( '.message' )
257+ if ( messages . length >= 2 ) {
258+ const userMessage = messages [ messages . length - 2 ]
259+ userMessage . scrollIntoView ( { behavior : 'smooth' , block : 'start' } )
260+ }
238261 }
239262 } )
240263 }
@@ -248,15 +271,33 @@ const AISearchDialog = {
248271 }
249272 } )
250273
274+ function renderContent ( msg ) {
275+ console . log ( 'msg' , JSON . stringify ( msg , null , 2 ) )
276+
277+ let content = msg . content
278+ if ( content . includes ( '${[' ) ) {
279+ content = content . replaceAll ( / \$ \{ ( \[ .* ?\] ) \} / g, ( match , group ) => {
280+ const ids = extractAllReferenceIds ( group )
281+ return ids . join ( ', ' )
282+ } )
283+ }
284+
285+ content = parseAndReplaceReferences ( msg . content , msg . hits )
286+
287+ const html = renderMarkdown ( content )
288+ return html
289+ }
290+
251291 return {
252292 refMessage,
253293 messages,
254294 inputMessage,
255295 loading,
256296 sendMessage,
257297 clearConversation,
258- renderMarkdown ,
298+ renderContent ,
259299 getUniqueHits,
300+ suggestions,
260301 }
261302 }
262303}
@@ -303,3 +344,98 @@ export default {
303344 }
304345 }
305346}
347+
348+ function getHit ( hits , id ) {
349+ return id && hits . find ( ( x ) => x . id == id || x . objectID == id )
350+ }
351+
352+ function refHtml ( hit ) {
353+ return `<a href="${ hit . url } " target="_blank" class="svg-external" title="${ htmlEncode ( hit . title ) } ">${ hit . id } </a>`
354+ }
355+
356+ /**
357+ * Parses markdown for reference patterns and replaces them with formatted links.
358+ * Supports patterns like: (Ref: 1234), (Ref. 1234), (id: 1234), (1234), [ref: 1234], [1234], [[1234]], etc.
359+ * Also supports 40-character UUIDs like [33ffd70e82ebd01b19dadc908ea097844c6fb013]
360+ *
361+ * @param {string } markdown - The markdown content to parse
362+ * @param {Object.<string, {id: string, title: string}> } hits - Dictionary of search results keyed by id
363+ * @param {Function } [replaceFn] - Optional callback function that receives the search result and returns HTML.
364+ * If not provided, uses default formatting: <a class="svg-external" title="{title}">{id}</a>
365+ * Signature: (searchResult) => string
366+ * @returns {string } Markdown with references replaced by formatted links
367+ */
368+ export function parseAndReplaceReferences ( markdown , hits , replaceFn ) {
369+ if ( ! markdown || ! hits || hits . length === 0 ) {
370+ return markdown
371+ }
372+
373+ // Regex pattern to match various reference formats:
374+ // - (Ref: 1234), (Ref. 1234), (Reference: 1234), (References: 1234), (id: 1234), (1234)
375+ // - [ref: 1234], [1234], [[1234]]
376+ // - [40-char-uuid] like [33ffd70e82ebd01b19dadc908ea097844c6fb013]
377+ // - Multiple references: (Ref. 1234, 1235) - extracts the first id
378+ const referencePattern = / \( (?: (?: R e f (?: e r e n c e ) ? s ? \. ? | i d ) : \s * ) ? ( \d + ) (?: \s * , \s * \d + ) * \) | \[ (?: r e f : \s * ) ? ( \d + ) \] | \[ \[ ( \d + ) \] \] | \[ ( [ a - f 0 - 9 ] { 40 } ) (?: \s * , \s * [ a - f 0 - 9 ] { 40 } ) * \] / gi
379+
380+ return markdown . replace ( referencePattern , ( match , group1 , group2 , group3 , group4 ) => {
381+ // Extract the first captured group that matched
382+ const id = group1 || group2 || group3 || group4
383+
384+ // If we found an id and it exists in search results, replace with formatted link
385+ const hit = getHit ( hits , id )
386+ if ( hit ) {
387+ console . log ( 'found' , id , hit )
388+
389+ // Use custom replaceFn if provided, otherwise use default formatting
390+ if ( replaceFn && typeof replaceFn === 'function' ) {
391+ return replaceFn ( hit )
392+ }
393+
394+ // Default formatting
395+ return refHtml ( hit )
396+ } else {
397+ console . log ( 'not found' , id )
398+ }
399+
400+ // Return original match if no search result found
401+ return match
402+ } )
403+ }
404+
405+ /**
406+ * Extracts the first reference id from markdown content.
407+ *
408+ * @param {string } markdown - The markdown content to parse
409+ * @returns {string|null } The first reference id found, or null if none found
410+ */
411+ export function extractFirstReferenceId ( markdown ) {
412+ if ( ! markdown ) return null
413+
414+ const referencePattern = / \( (?: (?: R e f (?: e r e n c e ) ? s ? \. ? | i d ) : \s * ) ? ( \d + ) (?: \s * , \s * \d + ) * \) | \[ (?: r e f : \s * ) ? ( \d + ) \] | \[ \[ ( \d + ) \] \] | \[ ( [ a - f 0 - 9 ] { 40 } ) \] / i
415+ const match = markdown . match ( referencePattern )
416+
417+ if ( ! match ) return null
418+
419+ return match [ 1 ] || match [ 2 ] || match [ 3 ] || match [ 4 ]
420+ }
421+
422+ /**
423+ * Extracts all reference ids from markdown content.
424+ *
425+ * @param {string } markdown - The markdown content to parse
426+ * @returns {string[] } List of all unique reference ids found
427+ */
428+ export function extractAllReferenceIds ( markdown ) {
429+ if ( ! markdown ) return [ ]
430+
431+ const referencePattern = / \( (?: (?: R e f (?: e r e n c e ) ? s ? \. ? | i d ) : \s * ) ? ( \d + ) (?: \s * , \s * \d + ) * \) | \[ (?: r e f : \s * ) ? ( \d + ) \] | \[ \[ ( \d + ) \] \] | \[ ( [ a - f 0 - 9 ] { 40 } ) \] / gi
432+ const ids = new Set ( )
433+ let match
434+
435+ while ( ( match = referencePattern . exec ( markdown ) ) !== null ) {
436+ const id = match [ 1 ] || match [ 2 ] || match [ 3 ] || match [ 4 ]
437+ if ( id ) ids . add ( id )
438+ }
439+
440+ return Array . from ( ids )
441+ }
0 commit comments