mirror of
https://github.com/coollabsio/coolify-docs.git
synced 2026-06-19 07:35:55 +00:00
feat: korrektly search
This commit is contained in:
+1
-1
@@ -5,4 +5,4 @@ VITE_SITE_URL=https://coolify.io/docs/
|
||||
|
||||
# Analytics domain for Plausible
|
||||
# Default: coolify.io/docs
|
||||
VITE_ANALYTICS_DOMAIN=coolify.io/docs
|
||||
VITE_ANALYTICS_DOMAIN=coolify.io/docs
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Update Korrektly
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v4.x
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Update Korrektly Chunks
|
||||
env:
|
||||
KORREKTLY_BASE_URL: ${{ secrets.KORREKTLY_BASE_URL }}
|
||||
KORREKTLY_API_TOKEN: ${{ secrets.KORREKTLY_API_TOKEN }}
|
||||
KORREKTLY_DATASET_ID: ${{ secrets.KORREKTLY_DATASET_ID }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: bunx @korrektly/vitepress --path . -r https://coolify.io -a docs/api-reference/api/operations
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Update Trieve
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v4.x
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install Trieve Vitepress Adapter
|
||||
run: bun install -g trieve-vitepress-adapter
|
||||
|
||||
- name: Update Trieve Chunks
|
||||
env:
|
||||
TRIEVE_API_HOST: ${{ secrets.TRIEVE_API_HOST }}
|
||||
TRIEVE_API_KEY: ${{ secrets.TRIEVE_API_KEY }}
|
||||
TRIEVE_ORGANIZATION_ID: ${{ secrets.TRIEVE_ORGANIZATION_ID }}
|
||||
TRIEVE_DATASET_TRACKING_ID: ${{ secrets.TRIEVE_DATASET_TRACKING_ID }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: trieve-vitepress-adapter --path . -s https://coolify.io/docs/openapi.json -r https://coolify.io -a docs/api-reference/api/operations
|
||||
@@ -58,7 +58,6 @@ export default defineConfig({
|
||||
['link', { rel: 'icon', href: '/docs/coolify-logo-transparent.png', alt: "Coolify's Logo" }],
|
||||
['link', { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
|
||||
['script', { defer: 'true', src: 'https://analytics.coollabs.io/js/script.tagged-events.js', 'data-domain': env.VITE_ANALYTICS_DOMAIN ?? 'coolify.io/docs' }],
|
||||
['script', { async: 'true', src: '/docs/trieve-user-script.js' }],
|
||||
],
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
@@ -566,6 +565,11 @@ export default defineConfig({
|
||||
}),
|
||||
],
|
||||
assetsInclude: ['**/*.yml'],
|
||||
define: {
|
||||
'import.meta.env.VITE_KORREKTLY_BASE_URL': JSON.stringify(env.KORREKTLY_BASE_URL || env.VITE_KORREKTLY_BASE_URL || ''),
|
||||
'import.meta.env.VITE_KORREKTLY_API_TOKEN': JSON.stringify(env.KORREKTLY_API_TOKEN || env.VITE_KORREKTLY_API_TOKEN || ''),
|
||||
'import.meta.env.VITE_KORREKTLY_DATASET_ID': JSON.stringify(env.KORREKTLY_DATASET_ID || env.VITE_KORREKTLY_DATASET_ID || ''),
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 5000
|
||||
},
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import { onKeyStroke } from '@vueuse/core'
|
||||
|
||||
const { isDark } = useData()
|
||||
|
||||
interface SearchResult {
|
||||
id: string
|
||||
title: string
|
||||
content: string
|
||||
url: string
|
||||
highlight?: string
|
||||
hierarchy?: string
|
||||
breadcrumb?: string
|
||||
}
|
||||
|
||||
const isOpen = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const searchResults = ref<SearchResult[]>([])
|
||||
const isLoading = ref(false)
|
||||
const selectedIndex = ref(0)
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null)
|
||||
const searchError = ref<string | null>(null)
|
||||
|
||||
// Korrektly SDK configuration
|
||||
const korrektlyConfig = {
|
||||
baseUrl: 'https://korrektly.com',
|
||||
apiToken: 'kly_pub_iUEPNafSehwGUVKpG4hW2OTj9qwFFn5CYTjaQLFo',
|
||||
datasetId: '019a220d-e4f3-733f-b405-f8457b7bc8ac',
|
||||
}
|
||||
|
||||
// Initialize Korrektly SDK
|
||||
let korrektlySDK: any = null
|
||||
|
||||
const openSearch = () => {
|
||||
isOpen.value = true
|
||||
setTimeout(() => {
|
||||
searchInputRef.value?.focus()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { Korrektly } = await import('@korrektly/sdk')
|
||||
korrektlySDK = new Korrektly({
|
||||
baseUrl: korrektlyConfig.baseUrl,
|
||||
apiToken: korrektlyConfig.apiToken,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Korrektly SDK:', error)
|
||||
}
|
||||
})
|
||||
|
||||
// Expose openSearch method so it can be called from parent components
|
||||
defineExpose({
|
||||
openSearch
|
||||
})
|
||||
|
||||
const closeSearch = () => {
|
||||
isOpen.value = false
|
||||
searchQuery.value = ''
|
||||
searchResults.value = []
|
||||
selectedIndex.value = 0
|
||||
searchError.value = null
|
||||
}
|
||||
|
||||
// Keyboard shortcuts (only for modal interactions)
|
||||
onKeyStroke('Escape', () => {
|
||||
if (isOpen.value) {
|
||||
closeSearch()
|
||||
}
|
||||
})
|
||||
|
||||
onKeyStroke('ArrowDown', (e) => {
|
||||
if (isOpen.value && searchResults.value.length > 0) {
|
||||
e.preventDefault()
|
||||
selectedIndex.value = Math.min(selectedIndex.value + 1, searchResults.value.length - 1)
|
||||
}
|
||||
})
|
||||
|
||||
onKeyStroke('ArrowUp', (e) => {
|
||||
if (isOpen.value && searchResults.value.length > 0) {
|
||||
e.preventDefault()
|
||||
selectedIndex.value = Math.max(selectedIndex.value - 1, 0)
|
||||
}
|
||||
})
|
||||
|
||||
onKeyStroke('Enter', () => {
|
||||
if (isOpen.value && searchResults.value[selectedIndex.value]) {
|
||||
navigateToResult(searchResults.value[selectedIndex.value])
|
||||
}
|
||||
})
|
||||
|
||||
// Debounced search
|
||||
let searchTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
watch(searchQuery, async (newQuery) => {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
}
|
||||
|
||||
if (!newQuery.trim()) {
|
||||
searchResults.value = []
|
||||
searchError.value = null
|
||||
return
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(async () => {
|
||||
await performSearch(newQuery)
|
||||
}, 300)
|
||||
})
|
||||
|
||||
const performSearch = async (query: string) => {
|
||||
if (!korrektlySDK) {
|
||||
console.error('Korrektly SDK not initialized')
|
||||
searchError.value = 'Search service not initialized. Please try refreshing the page.'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
searchError.value = null
|
||||
try {
|
||||
const response = await korrektlySDK.search(korrektlyConfig.datasetId, {
|
||||
query,
|
||||
limit: 10,
|
||||
search_type: 'hybrid',
|
||||
})
|
||||
|
||||
// Check if response contains an error
|
||||
if (response?.error || response?.message) {
|
||||
const errorMessage = response.message || response.error || 'An unknown error occurred'
|
||||
searchError.value = errorMessage
|
||||
searchResults.value = []
|
||||
console.error('Search API error:', response)
|
||||
return
|
||||
}
|
||||
|
||||
// The API returns { success, data: { results: [...] } }
|
||||
const results = response?.data?.results || response?.results || response?.chunks || []
|
||||
|
||||
searchResults.value = results.map((chunk: any) => {
|
||||
// Extract metadata from array format
|
||||
const getMetadata = (key: string) => {
|
||||
const meta = chunk.metadata?.find((m: any) => m.key === key)
|
||||
return meta?.value || ''
|
||||
}
|
||||
|
||||
const title = getMetadata('title') || getMetadata('heading') || extractTitle(chunk.content_html) || 'Untitled'
|
||||
const description = getMetadata('description') || ''
|
||||
const hierarchy = getMetadata('hierarchy') || ''
|
||||
|
||||
// Build URL from source_url or group tracking_id
|
||||
let url = chunk.source_url || ''
|
||||
if (url.includes('/home/aditya/workspace/coollabs/coolify-docs/docs')) {
|
||||
// Convert file path to URL path
|
||||
url = url.replace('/home/aditya/workspace/coollabs/coolify-docs/docs', '/docs')
|
||||
url = url.replace('.md', '')
|
||||
} else if (chunk.group?.tracking_id) {
|
||||
url = chunk.group.tracking_id.replace('/home/aditya/workspace/coollabs/coolify-docs/docs', '/docs')
|
||||
url = url.replace('.md', '')
|
||||
}
|
||||
|
||||
// Create breadcrumb from hierarchy or URL (excluding 'docs' prefix)
|
||||
let breadcrumb = ''
|
||||
if (hierarchy) {
|
||||
// Convert "home > aditya > workspace > coollabs > coolify-docs > docs > services > n8n"
|
||||
// to "services / n8n"
|
||||
const parts = hierarchy.split(' > ')
|
||||
const docsIndex = parts.indexOf('docs')
|
||||
if (docsIndex !== -1 && docsIndex < parts.length - 1) {
|
||||
breadcrumb = parts.slice(docsIndex + 1).join(' / ')
|
||||
} else {
|
||||
breadcrumb = hierarchy.replace(/ > /g, ' / ')
|
||||
}
|
||||
} else if (url) {
|
||||
// Extract from URL: /docs/services/n8n -> services / n8n
|
||||
breadcrumb = url.replace(/^\/docs\//, '').replace(/\//g, ' / ')
|
||||
}
|
||||
|
||||
return {
|
||||
id: chunk.id,
|
||||
title,
|
||||
content: description || chunk.content_html || chunk.content || '',
|
||||
url,
|
||||
highlight: chunk.content_html,
|
||||
hierarchy,
|
||||
breadcrumb,
|
||||
}
|
||||
})
|
||||
|
||||
selectedIndex.value = 0
|
||||
} catch (error: any) {
|
||||
console.error('Search error:', error)
|
||||
|
||||
// Try to extract error message from the error object
|
||||
let errorMessage = 'An unexpected error occurred while searching.'
|
||||
|
||||
if (error?.response?.data?.message) {
|
||||
errorMessage = error.response.data.message
|
||||
} else if (error?.message) {
|
||||
errorMessage = error.message
|
||||
} else if (typeof error === 'string') {
|
||||
errorMessage = error
|
||||
}
|
||||
|
||||
searchError.value = errorMessage
|
||||
searchResults.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const extractTitle = (html: string): string => {
|
||||
if (!html) return ''
|
||||
const temp = document.createElement('div')
|
||||
temp.innerHTML = html
|
||||
const heading = temp.querySelector('h1, h2, h3, h4, h5, h6')
|
||||
return heading?.textContent?.trim() || ''
|
||||
}
|
||||
|
||||
const navigateToResult = (result: SearchResult) => {
|
||||
window.location.href = result.url
|
||||
closeSearch()
|
||||
}
|
||||
|
||||
const handleBackdropClick = (e: MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
closeSearch()
|
||||
}
|
||||
}
|
||||
|
||||
const stripHtml = (html: string) => {
|
||||
const temp = document.createElement('div')
|
||||
temp.innerHTML = html
|
||||
return temp.textContent || temp.innerText || ''
|
||||
}
|
||||
|
||||
const truncate = (text: string, length: number) => {
|
||||
if (text.length <= length) return text
|
||||
return text.substring(0, length) + '...'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-200"
|
||||
leave-active-class="transition-opacity duration-200"
|
||||
enter-from-class="opacity-0"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 z-[9999] flex items-start justify-center overflow-y-auto bg-black/50 backdrop-blur-sm p-4 pt-[10vh]"
|
||||
@click="handleBackdropClick"
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-200"
|
||||
leave-active-class="transition-all duration-200"
|
||||
enter-from-class="opacity-0 -translate-y-5"
|
||||
leave-to-class="opacity-0 -translate-y-5"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="w-full max-w-2xl bg-[var(--vp-c-bg)] rounded-xl shadow-2xl border border-[var(--vp-c-divider)] flex flex-col max-h-[80vh]"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Search Header -->
|
||||
<div class="p-4 border-b border-[var(--vp-c-divider)]">
|
||||
<div class="relative flex items-center">
|
||||
<svg class="absolute left-3 w-5 h-5 text-[var(--vp-c-text-2)] pointer-events-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search documentation... (⌘K or Ctrl+K)"
|
||||
class="w-full pl-11 pr-10 py-3 text-base bg-[var(--vp-c-bg-soft)] rounded-lg border-none outline-none text-[var(--vp-c-text-1)] placeholder:text-[var(--vp-c-text-3)] focus:bg-[var(--vp-c-bg-elv)] transition-colors"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
class="absolute right-3 w-6 h-6 flex items-center justify-center rounded bg-[var(--vp-c-bg-soft)] text-[var(--vp-c-text-2)] hover:bg-[var(--vp-c-bg-elv)] hover:text-[var(--vp-c-text-1)] transition-all text-xl leading-none"
|
||||
@click="searchQuery = ''"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Body -->
|
||||
<div class="flex-1 overflow-y-auto min-h-[200px] max-h-[500px]">
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center py-12 px-6 text-[var(--vp-c-text-2)]">
|
||||
<div class="w-8 h-8 border-3 border-[var(--vp-c-divider)] border-t-[var(--vp-c-brand)] rounded-full animate-spin mb-4"></div>
|
||||
<p>Searching...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="searchError" class="flex flex-col items-center justify-center py-12 px-6 text-center">
|
||||
<div class="max-w-md">
|
||||
<div class="mb-4 text-4xl">⚠️</div>
|
||||
<h3 class="text-lg font-semibold text-[var(--vp-c-text-1)] mb-3">Search Error</h3>
|
||||
<div class="bg-[var(--vp-c-bg-soft)] border border-[var(--vp-c-divider)] rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-[var(--vp-c-text-2)] font-mono break-words">{{ searchError }}</p>
|
||||
</div>
|
||||
<p class="text-sm text-[var(--vp-c-text-2)] mb-4">
|
||||
If this issue persists, please contact support for assistance.
|
||||
</p>
|
||||
<a
|
||||
href="mailto:support@korrektly.com"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-[var(--vp-c-brand)] text-white rounded-lg hover:bg-[var(--vp-c-brand-dark)] transition-colors no-underline text-sm font-medium"
|
||||
>
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
|
||||
</svg>
|
||||
Contact Korrektly Support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="searchQuery && searchResults.length === 0" class="flex items-center justify-center py-12 px-6 text-center text-[var(--vp-c-text-2)]">
|
||||
<p>No results found for "{{ searchQuery }}"</p>
|
||||
</div>
|
||||
|
||||
<!-- Results List -->
|
||||
<div v-else-if="searchResults.length > 0" class="p-2">
|
||||
<a
|
||||
v-for="(result, index) in searchResults"
|
||||
:key="result.id"
|
||||
:href="result.url"
|
||||
class="block p-3 mb-1 rounded-lg cursor-pointer no-underline transition-all border border-transparent hover:bg-[var(--vp-c-bg-soft)] hover:border-[var(--vp-c-brand)]"
|
||||
:class="{ 'bg-[var(--vp-c-bg-soft)] border-[var(--vp-c-brand)]': index === selectedIndex }"
|
||||
@click.prevent="navigateToResult(result)"
|
||||
@mouseenter="selectedIndex = index"
|
||||
>
|
||||
<div class="mb-1.5">
|
||||
<div class="font-semibold text-sm text-[var(--vp-c-text-1)] mb-0.5">{{ result.title }}</div>
|
||||
<div v-if="result.breadcrumb" class="flex items-center gap-1 text-[11px] text-[var(--vp-c-text-3)] font-mono opacity-80 mt-0.5">
|
||||
<span class="text-[10px] opacity-60">📁</span>
|
||||
<span>{{ result.breadcrumb }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[13px] text-[var(--vp-c-text-2)] leading-relaxed line-clamp-2">
|
||||
{{ truncate(stripHtml(result.content), 150) }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Initial State -->
|
||||
<div v-else class="py-12 px-6 text-center text-[var(--vp-c-text-2)]">
|
||||
<p class="text-sm mb-6">Start typing to search...</p>
|
||||
<div class="mt-6">
|
||||
<p class="text-xs font-semibold text-[var(--vp-c-text-2)] mb-3 uppercase tracking-wide">Popular searches:</p>
|
||||
<div class="flex flex-wrap gap-2 justify-center">
|
||||
<button
|
||||
v-for="tag in ['Backups', 'PostgreSQL', 'Docker Compose', 'GitHub Actions']"
|
||||
:key="tag"
|
||||
class="px-3 py-1.5 bg-[var(--vp-c-bg-soft)] border border-[var(--vp-c-divider)] rounded-md text-[13px] text-[var(--vp-c-text-1)] hover:bg-[var(--vp-c-brand)] hover:text-white hover:border-[var(--vp-c-brand)] transition-all cursor-pointer"
|
||||
@click="searchQuery = tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Footer -->
|
||||
<div class="px-4 py-3 border-t border-[var(--vp-c-divider)] flex justify-between items-center text-xs text-[var(--vp-c-text-2)]">
|
||||
<div class="hidden md:flex gap-3">
|
||||
<span class="flex items-center gap-1">
|
||||
<kbd class="px-1.5 py-0.5 bg-[var(--vp-c-bg-soft)] border border-[var(--vp-c-divider)] rounded text-[11px] font-mono">↑</kbd>
|
||||
<kbd class="px-1.5 py-0.5 bg-[var(--vp-c-bg-soft)] border border-[var(--vp-c-divider)] rounded text-[11px] font-mono">↓</kbd>
|
||||
<span class="ml-1">Navigate</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<kbd class="px-1.5 py-0.5 bg-[var(--vp-c-bg-soft)] border border-[var(--vp-c-divider)] rounded text-[11px] font-mono">↵</kbd>
|
||||
<span class="ml-1">Select</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<kbd class="px-1.5 py-0.5 bg-[var(--vp-c-bg-soft)] border border-[var(--vp-c-divider)] rounded text-[11px] font-mono">ESC</kbd>
|
||||
<span class="ml-1">Close</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Powered by <a href="https://korrektly.com" target="_blank" rel="noopener" class="text-[var(--vp-c-brand)] no-underline hover:underline">Korrektly</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -0,0 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, onMounted } from 'vue'
|
||||
import { onKeyStroke } from '@vueuse/core'
|
||||
|
||||
// Inject the openSearch function from KorrektlySearch (with global fallback)
|
||||
const openSearchInjected = inject<() => void>('openKorrektlySearch', () => {})
|
||||
|
||||
const openSearch = () => {
|
||||
// Try injected function first
|
||||
if (openSearchInjected && typeof openSearchInjected === 'function') {
|
||||
openSearchInjected()
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to global window reference
|
||||
if (typeof window !== 'undefined' && (window as any).__korrektlySearch) {
|
||||
(window as any).__korrektlySearch.openSearch()
|
||||
} else {
|
||||
console.warn('KorrektlySearch not available')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle keyboard shortcut (Cmd/Ctrl+K)
|
||||
onKeyStroke(['k', 'K'], (e) => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
openSearch()
|
||||
}
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
openSearch()
|
||||
}
|
||||
|
||||
// Detect Mac for keyboard shortcut display
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined' && /(mac|iphone|ipod|ipad)/i.test(navigator.platform)) {
|
||||
document.documentElement.classList.add('mac')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPNavBarSearch search" @click="handleClick">
|
||||
<button
|
||||
type="button"
|
||||
class="DocSearch DocSearch-Button"
|
||||
aria-label="Search"
|
||||
>
|
||||
<span class="DocSearch-Button-Container">
|
||||
<svg
|
||||
class="DocSearch-Search-Icon"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="DocSearch-Button-Placeholder">Search</span>
|
||||
</span>
|
||||
<span class="DocSearch-Button-Keys">
|
||||
<kbd class="DocSearch-Button-Key"></kbd>
|
||||
<kbd class="DocSearch-Button-Key">K</kbd>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarSearch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBarSearch {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand-1);
|
||||
--docsearch-text-color: var(--vp-c-text-1);
|
||||
--docsearch-spacing: 12px;
|
||||
--docsearch-icon-stroke-width: 1.4;
|
||||
--docsearch-highlight-color: var(--docsearch-primary-color);
|
||||
--docsearch-muted-color: var(--vp-c-text-2);
|
||||
--docsearch-container-background: rgba(101, 108, 133, 0.8);
|
||||
--docsearch-modal-background: var(--vp-c-bg-elv);
|
||||
--docsearch-searchbox-background: var(--vp-c-bg-alt);
|
||||
--docsearch-searchbox-focus-background: var(--vp-c-bg);
|
||||
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);
|
||||
--docsearch-hit-color: var(--vp-c-text-2);
|
||||
--docsearch-hit-active-color: var(--vp-c-text-1);
|
||||
--docsearch-hit-background: var(--vp-c-default-soft);
|
||||
--docsearch-hit-shadow: none;
|
||||
--docsearch-footer-background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.DocSearch-Button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0 12px 0 14px;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: border-color 0.25s, background-color 0.25s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.DocSearch-Button-Container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.DocSearch-Search-Icon {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--vp-c-text-2);
|
||||
fill: none;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover .DocSearch-Search-Icon {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.DocSearch-Button-Placeholder {
|
||||
display: none;
|
||||
padding: 0 10px 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover .DocSearch-Button-Placeholder {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button-Placeholder {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button-Keys {
|
||||
direction: ltr;
|
||||
display: none;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button-Keys {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button-Key {
|
||||
display: block;
|
||||
margin: 2px 0;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-right: none;
|
||||
border-radius: 3px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
font-family: var(--vp-font-family-base);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: color 0.25s, border-color 0.25s;
|
||||
}
|
||||
|
||||
.DocSearch-Button-Key:first-child {
|
||||
font-size: 0 !important;
|
||||
}
|
||||
|
||||
.DocSearch-Button-Key:first-child::after {
|
||||
content: "⌘";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
html:not(.mac) .DocSearch-Button-Key:first-child::after {
|
||||
content: "Ctrl";
|
||||
}
|
||||
|
||||
.DocSearch-Button-Key:last-child {
|
||||
border-right: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBarSearch {
|
||||
flex-grow: 1;
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBarSearch {
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@ import { inBrowser } from 'vitepress'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useSidebar } from 'vitepress/theme'
|
||||
import VPSidebarGroup from 'vitepress/dist/client/theme-default/components/VPSidebarGroup.vue'
|
||||
import VPNavBarSearch from 'vitepress/dist/client/theme-default/components/VPNavBarSearch.vue'
|
||||
import VPNavBarSearch from './VPNavBarSearch.vue'
|
||||
import VPNavBarAppearance from 'vitepress/dist/client/theme-default/components/VPNavBarAppearance.vue'
|
||||
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'
|
||||
|
||||
@@ -48,7 +48,7 @@ watch(
|
||||
|
||||
|
||||
<nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
|
||||
<VPNavBarSearch class="sm:block search w-full my-auto px-0 " />
|
||||
<VPNavBarSearch class="sidebar-search" />
|
||||
|
||||
<span class="visually-hidden" id="sidebar-aria-label">
|
||||
Sidebar Navigation
|
||||
@@ -134,4 +134,22 @@ watch(
|
||||
.nav {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.sidebar-search {
|
||||
padding-left: 0 !important;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sidebar-search :deep(.DocSearch-Button) {
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
.sidebar-search :deep(.DocSearch-Button-Placeholder) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.sidebar-search :deep(.DocSearch-Button-Keys) {
|
||||
display: flex !important;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -33,6 +33,7 @@ import TabBlock from "./components/TabBlock.vue";
|
||||
import ZoomableImage from "./components/ZoomableImage.vue";
|
||||
import Globe from "./components/Landing/Globe.vue";
|
||||
import Browser from "./components/Landing/Browser.vue";
|
||||
import KorrektlySearch from "./components/KorrektlySearch.vue";
|
||||
|
||||
// Import Vdoc overrides
|
||||
import VPDoc from "./components/VPDoc.vue";
|
||||
@@ -80,6 +81,7 @@ export default {
|
||||
app.component("ZoomableImage", ZoomableImage);
|
||||
app.component("Globe", Globe);
|
||||
app.component("Browser", Browser);
|
||||
app.component("KorrektlySearch", KorrektlySearch);
|
||||
|
||||
// Register Vdoc overrides
|
||||
app.component("VPDoc", VPDoc);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<KorrektlySearch ref="korrektlySearchRef" />
|
||||
<Layout>
|
||||
<template #home-features-before>
|
||||
</template>
|
||||
@@ -6,9 +7,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, provide, onMounted } from 'vue'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { useData } from 'vitepress'
|
||||
import KorrektlySearch from '../components/KorrektlySearch.vue'
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
const { frontmatter } = useData()
|
||||
|
||||
// Create ref to KorrektlySearch component
|
||||
const korrektlySearchRef = ref<InstanceType<typeof KorrektlySearch> | null>(null)
|
||||
|
||||
// Provide the openSearch function to all child components
|
||||
provide('openKorrektlySearch', () => {
|
||||
korrektlySearchRef.value?.openSearch()
|
||||
})
|
||||
|
||||
// Also expose globally for easier access
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).__korrektlySearch = korrektlySearchRef.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,97 +0,0 @@
|
||||
// ==UserScript==
|
||||
// @name Coolify
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 2025-01-16
|
||||
// @description try to take over the world!
|
||||
// @author You
|
||||
// @match https://coolify.io/docs/*
|
||||
// @icon https://www.google.com/s2/favicons?sz=64&domain=coolify.io
|
||||
// @grant none
|
||||
// ==/UserScript==
|
||||
|
||||
const removeAllClickListeners = (element) => {
|
||||
const newElement = element.cloneNode(true);
|
||||
element.parentNode.replaceChild(newElement, element);
|
||||
return newElement;
|
||||
};
|
||||
|
||||
const makeDefaultSearchTrieve = async () => {
|
||||
let defaultSearchBar = null;
|
||||
let retries = 0;
|
||||
while (!defaultSearchBar && retries < 10) {
|
||||
for (const el of document.querySelectorAll("*")) {
|
||||
if (el.querySelector('#local-search > button')) {
|
||||
defaultSearchBar = el.querySelector('#local-search > button');
|
||||
break;
|
||||
}
|
||||
}
|
||||
retries++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
if (!defaultSearchBar) {
|
||||
return;
|
||||
}
|
||||
|
||||
defaultSearchBar = removeAllClickListeners(defaultSearchBar);
|
||||
|
||||
defaultSearchBar.onclick = () => {
|
||||
const event = new CustomEvent("trieve-open-with-text", {
|
||||
detail: { text: "" },
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
};
|
||||
};
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
makeDefaultSearchTrieve();
|
||||
});
|
||||
|
||||
const originalPushState = history.pushState;
|
||||
history.pushState = function() {
|
||||
originalPushState.apply(this, arguments);
|
||||
makeDefaultSearchTrieve();
|
||||
};
|
||||
|
||||
|
||||
(async function () {
|
||||
"use strict";
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = "https://cdn.trieve.ai/beta/search-component/index.css";
|
||||
document.head.appendChild(link);
|
||||
|
||||
import("https://cdn.trieve.ai/beta/search-component/vanilla/index.js").then(
|
||||
async (module) => {
|
||||
const { renderToDiv } = module;
|
||||
const root = document.createElement("div");
|
||||
root.classList.add("trigger");
|
||||
document.body.appendChild(root);
|
||||
const colorScheme = document.documentElement?.classList?.contains("dark")
|
||||
? "dark"
|
||||
: null;
|
||||
|
||||
renderToDiv(root, {
|
||||
apiKey: "tr-4ge266qRg6AzfMAyWyqqUjmG3VC1CYYM",
|
||||
datasetId: "cae68afa-93e1-4fb2-9945-693e65906409",
|
||||
baseUrl: "https://api.trieve.ai",
|
||||
type: "docs",
|
||||
analytics: true,
|
||||
theme: colorScheme === "dark" ? "dark" : null,
|
||||
brandLogoImgSrcUrl: "https://coolify.io/docs/coolify-logo-transparent.png",
|
||||
brandName: "Coolify",
|
||||
brandColor: "#9664f3",
|
||||
placeholder: "How can I help?",
|
||||
defaultSearchQueries: ["Backups", "Postgresql", "Private NPM registry"],
|
||||
defaultAiQuestions: ["How to fix expired GitHub personal access token (PAT)?", "My Raspberry Pi is crashing", "How to use Docker Compose?"],
|
||||
defaultSearchMode: "search",
|
||||
showFloatingButton: "true",
|
||||
cssRelease: "none",
|
||||
hideOpenButton: true,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
console.error("Failed to load module:", error);
|
||||
}
|
||||
);
|
||||
})();
|
||||
+2
-1
@@ -34,9 +34,10 @@
|
||||
"transform-openapi": "tsx scripts/convert-openapi.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@korrektly/sdk": "^0.1.1",
|
||||
"@vueuse/core": "12.5.0",
|
||||
"globe.gl": "2.39.7",
|
||||
"vitepress-openapi": "0.0.3-alpha.78"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.3+sha512.bb45e34d50a9a76e858a95837301bfb6bd6d35aea2c5d52094fa497a467c43f5c440103ce2511e9e0a2f89c3d6071baac3358fc68ac6fb75e2ceb3d2736065e6"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user