chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { ButtonVariantType } from '@/components/ui/Button'
|
||||
import { normalizeApiPrefix, normalizeWebuiPrefix } from '@/lib/pathPrefix'
|
||||
import { getRuntimeApiPrefix, getRuntimeWebuiPrefix } from '@/lib/runtimeConfig'
|
||||
|
||||
export const backendBaseUrl = normalizeApiPrefix(getRuntimeApiPrefix())
|
||||
export const webuiPrefix = normalizeWebuiPrefix(getRuntimeWebuiPrefix())
|
||||
|
||||
export const controlButtonVariant: ButtonVariantType = 'ghost'
|
||||
|
||||
export const labelColorDarkTheme = '#FFFFFF'
|
||||
export const LabelColorHighlightedDarkTheme = '#000000'
|
||||
export const labelColorLightTheme = '#000'
|
||||
|
||||
export const nodeColorDisabled = '#E2E2E2'
|
||||
export const nodeBorderColor = '#EEEEEE'
|
||||
export const nodeBorderColorSelected = '#F57F17'
|
||||
|
||||
export const edgeColorDarkTheme = '#888888'
|
||||
export const edgeColorSelected = '#F57F17'
|
||||
export const edgeColorHighlightedDarkTheme = '#F57F17'
|
||||
export const edgeColorHighlightedLightTheme = '#F57F17'
|
||||
|
||||
export const searchResultLimit = 50
|
||||
export const labelListLimit = 100
|
||||
|
||||
// Search History Configuration
|
||||
export const searchHistoryMaxItems = 500
|
||||
export const searchHistoryVersion = '1.0'
|
||||
|
||||
// API Request Limits
|
||||
export const popularLabelsDefaultLimit = 300
|
||||
export const searchLabelsDefaultLimit = 50
|
||||
|
||||
// UI Display Limits
|
||||
export const dropdownDisplayLimit = 300
|
||||
|
||||
export const minNodeSize = 4
|
||||
export const maxNodeSize = 20
|
||||
|
||||
export const healthCheckInterval = 15 // seconds
|
||||
|
||||
export const defaultQueryLabel = '*'
|
||||
|
||||
// reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types
|
||||
export const supportedFileTypes = {
|
||||
'text/plain': [
|
||||
'.txt',
|
||||
'.md',
|
||||
'.textpack', // # Markdown Bundle(zip)
|
||||
'.mdx', // # MDX (Markdown + JSX)
|
||||
'.rtf', // # Rich Text Format
|
||||
'.odt', // # OpenDocument Text
|
||||
'.tex', // # LaTeX
|
||||
'.epub', // # Electronic Publication
|
||||
'.html', // # HyperText Markup Language
|
||||
'.htm', // # HyperText Markup Language
|
||||
'.csv', // # Comma-Separated Values
|
||||
'.json', // # JavaScript Object Notation
|
||||
'.xml', // # eXtensible Markup Language
|
||||
'.yaml', // # YAML Ain't Markup Language
|
||||
'.yml', // # YAML
|
||||
'.log', // # Log files
|
||||
'.conf', // # Configuration files
|
||||
'.ini', // # Initialization files
|
||||
'.properties', // # Java properties files
|
||||
'.sql', // # SQL scripts
|
||||
'.bat', // # Batch files
|
||||
'.sh', // # Shell scripts
|
||||
'.c', // # C source code
|
||||
'.h', // # C header
|
||||
'.cpp', // # C++ source code
|
||||
'.hpp', // # C++ header
|
||||
'.py', // # Python source code
|
||||
'.java', // # Java source code
|
||||
'.js', // # JavaScript source code
|
||||
'.ts', // # TypeScript source code
|
||||
'.swift', // # Swift source code
|
||||
'.go', // # Go source code
|
||||
'.rb', // # Ruby source code
|
||||
'.php', // # PHP source code
|
||||
'.css', // # Cascading Style Sheets
|
||||
'.scss', // # Sassy CSS
|
||||
'.less'
|
||||
],
|
||||
'application/pdf': ['.pdf'],
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx']
|
||||
}
|
||||
|
||||
export const SiteInfo = {
|
||||
name: 'LightRAG',
|
||||
home: '/',
|
||||
github: 'https://github.com/HKUDS/LightRAG'
|
||||
}
|
||||
|
||||
// --- Graph layout performance thresholds ------------------------------------
|
||||
// Shared by the initial FA2 layout (GraphControl) and the manual worker
|
||||
// layouts (LayoutsControl) so the two cannot drift.
|
||||
|
||||
// Above this node count, node labels are forced off regardless of the
|
||||
// showNodeLabel setting (the hovered node's label is still drawn by sigma's
|
||||
// hover layer). Rendering thousands of labels is a major large-graph slowdown.
|
||||
export const LABEL_RENDER_LIMIT = 2000
|
||||
|
||||
// Above this node count, layout switches assign positions directly instead of
|
||||
// animating: animateNodes interpolates every node per frame on the main thread.
|
||||
export const ANIMATE_NODE_LIMIT = 5000
|
||||
|
||||
// Edge-count threshold that switches the graph between "small-graph experience"
|
||||
// and "large-graph performance". At or below it edges render as curves and edge
|
||||
// events (hover/click picking) follow the user setting; above it edges render
|
||||
// straight and edge events are fully disabled (no picking buffer allocated).
|
||||
// Shared by GraphControl (defaultEdgeType), GraphViewer (enableEdgeEvents
|
||||
// gating) and Settings (greying the Edge Events menu item) so they cannot drift.
|
||||
export const EDGE_PERF_LIMIT = 5000
|
||||
|
||||
// Time budget (ms) a relaxing worker layout runs before it is stopped. Scales
|
||||
// with graph size, capped so huge graphs don't run unbounded.
|
||||
export const workerBudgetMs = (order: number): number => Math.min(1500 + order / 10, 10000)
|
||||
|
||||
// One-time system-suggested user prompts, injected once into userPromptHistory
|
||||
// (for both fresh installs and upgrades). See settings store version 20 migration.
|
||||
export const suggestedUserPrompts: string[] = [
|
||||
'Ignore the `References Section Format` instruction in the system prompt, and do not include a `References` section in the response.',
|
||||
'For inline citations, use the footnote marker syntax `[^1]`, where the `^` preceding the identifier indicates a footnote reference. When multiple citations are required at a single location, each ID should be enclosed in separate footnote markers (e.g., `[^1][^2][^3]`).'
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file is for importing libraries that have global side effects.
|
||||
|
||||
// Load KaTeX mhchem extension globally
|
||||
import 'katex/contrib/mhchem';
|
||||
@@ -0,0 +1,67 @@
|
||||
/// <reference types="bun" />
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { normalizeApiPrefix, normalizeWebuiPrefix } from './pathPrefix'
|
||||
|
||||
describe('normalizeApiPrefix', () => {
|
||||
test('empty / undefined / null collapse to ""', () => {
|
||||
expect(normalizeApiPrefix(undefined)).toBe('')
|
||||
expect(normalizeApiPrefix(null)).toBe('')
|
||||
expect(normalizeApiPrefix('')).toBe('')
|
||||
expect(normalizeApiPrefix(' ')).toBe('')
|
||||
})
|
||||
|
||||
test('"/" collapses to "" — avoids `${"/"} + "/x"` producing protocol-relative `//x`', () => {
|
||||
expect(normalizeApiPrefix('/')).toBe('')
|
||||
})
|
||||
|
||||
test('strips trailing slashes (one or many)', () => {
|
||||
expect(normalizeApiPrefix('/api/v1/')).toBe('/api/v1')
|
||||
expect(normalizeApiPrefix('/api/v1//')).toBe('/api/v1')
|
||||
})
|
||||
|
||||
test('adds leading slash if missing', () => {
|
||||
expect(normalizeApiPrefix('api/v1')).toBe('/api/v1')
|
||||
})
|
||||
|
||||
test('passes canonical form through unchanged', () => {
|
||||
expect(normalizeApiPrefix('/api/v1')).toBe('/api/v1')
|
||||
})
|
||||
|
||||
test('result is safe for fetch template concat: never starts with `//` and never ends with `/`', () => {
|
||||
for (const input of ['', '/', undefined, '/api', '/api/', 'api', '/api/v1/']) {
|
||||
const out = normalizeApiPrefix(input)
|
||||
const fetchUrl = `${out}/query/stream`
|
||||
expect(fetchUrl.startsWith('//')).toBe(false)
|
||||
expect(fetchUrl).not.toContain('//')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeWebuiPrefix', () => {
|
||||
test('empty / undefined / null fall back to default with trailing slash', () => {
|
||||
expect(normalizeWebuiPrefix(undefined)).toBe('/webui/')
|
||||
expect(normalizeWebuiPrefix(null)).toBe('/webui/')
|
||||
expect(normalizeWebuiPrefix('')).toBe('/webui/')
|
||||
expect(normalizeWebuiPrefix(' ')).toBe('/webui/')
|
||||
})
|
||||
|
||||
test('"/" falls back to default — degenerate value rejected', () => {
|
||||
expect(normalizeWebuiPrefix('/')).toBe('/webui/')
|
||||
})
|
||||
|
||||
test('always ends with exactly one trailing slash (Vite `base` requirement)', () => {
|
||||
expect(normalizeWebuiPrefix('/admin/ui')).toBe('/admin/ui/')
|
||||
expect(normalizeWebuiPrefix('/admin/ui/')).toBe('/admin/ui/')
|
||||
expect(normalizeWebuiPrefix('/admin/ui//')).toBe('/admin/ui/')
|
||||
})
|
||||
|
||||
test('adds leading slash if missing', () => {
|
||||
expect(normalizeWebuiPrefix('admin/ui')).toBe('/admin/ui/')
|
||||
})
|
||||
|
||||
test('respects custom fallback', () => {
|
||||
expect(normalizeWebuiPrefix(undefined, '/custom')).toBe('/custom/')
|
||||
expect(normalizeWebuiPrefix('/', '/custom')).toBe('/custom/')
|
||||
expect(normalizeWebuiPrefix('', '/custom/')).toBe('/custom/')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Path prefix normalization utilities.
|
||||
*
|
||||
* Used by both the React source (where Vite statically replaces
|
||||
* `import.meta.env.VITE_*`) and `vite.config.ts` (where the config is loaded
|
||||
* by Node and only `loadEnv()` is reliable for reading user env vars).
|
||||
*
|
||||
* Keep this module dependency-free so it can be imported from `vite.config.ts`
|
||||
* without dragging in `@/...` path aliases or React/UI types.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalize an API path prefix used as `axios.baseURL` and as a string-concat
|
||||
* prefix in `fetch(`${prefix}/path`)` and iframe `src`.
|
||||
*
|
||||
* - Empty / undefined / "/" → "" (avoids producing `//path` which fetch
|
||||
* interprets as a protocol-relative URL pointing at host "path").
|
||||
* - Always returns either "" or a string with a leading "/" and no trailing "/".
|
||||
*/
|
||||
export function normalizeApiPrefix(value: string | undefined | null): string {
|
||||
if (!value) return ''
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed || trimmed === '/') return ''
|
||||
const withLeading = trimmed.startsWith('/') ? trimmed : '/' + trimmed
|
||||
return withLeading.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a WebUI mount path used as Vite's `base` option and as `<a href>`.
|
||||
*
|
||||
* Vite requires `base` to end with "/", and serving via `<a href={prefix}>`
|
||||
* also avoids a 307 redirect when the value already ends with "/".
|
||||
*
|
||||
* - Empty / undefined / "/" → fallback (default `/webui`) with trailing "/".
|
||||
* - Always returns a value with a leading "/" and exactly one trailing "/".
|
||||
*/
|
||||
export function normalizeWebuiPrefix(
|
||||
value: string | undefined | null,
|
||||
fallback = '/webui'
|
||||
): string {
|
||||
const trimmed = (value ?? '').trim()
|
||||
const candidate =
|
||||
!trimmed || trimmed === '/'
|
||||
? fallback
|
||||
: trimmed.startsWith('/')
|
||||
? trimmed
|
||||
: '/' + trimmed
|
||||
return candidate.replace(/\/+$/, '') + '/'
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Runtime path prefix configuration.
|
||||
*
|
||||
* The browser-visible URL prefixes (API base, WebUI mount path) used to be
|
||||
* baked into the bundle from `import.meta.env.VITE_*_PREFIX` at build time,
|
||||
* forcing one build per reverse-proxy mount point. They are now resolved at
|
||||
* REQUEST time: the FastAPI server replaces a `<!-- __LIGHTRAG_RUNTIME_CONFIG__ -->`
|
||||
* comment in `index.html` with a `<script>window.__LIGHTRAG_CONFIG__ = ...</script>`
|
||||
* snippet built from `LIGHTRAG_API_PREFIX` / `LIGHTRAG_WEBUI_PATH`. This module
|
||||
* is the single read point for that injected value.
|
||||
*
|
||||
* Dev parity: `vite.config.ts` performs the same injection via a
|
||||
* `transformIndexHtml` plugin using `VITE_DEV_API_PREFIX` /
|
||||
* `VITE_DEV_WEBUI_PREFIX`, so dev and prod use the same lookup.
|
||||
*
|
||||
* Always read through {@link normalizeApiPrefix} / {@link normalizeWebuiPrefix}
|
||||
* downstream (see `constants.ts`); this module returns the raw injected value.
|
||||
*/
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__LIGHTRAG_CONFIG__?: {
|
||||
apiPrefix?: string
|
||||
webuiPrefix?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config =
|
||||
(typeof window !== 'undefined' && window.__LIGHTRAG_CONFIG__) || {}
|
||||
|
||||
/** Browser-visible API prefix; empty string means same-origin / no prefix. */
|
||||
export function getRuntimeApiPrefix(): string | undefined {
|
||||
return config.apiPrefix
|
||||
}
|
||||
|
||||
/** Browser-visible WebUI mount path including the trailing slash. */
|
||||
export function getRuntimeWebuiPrefix(): string | undefined {
|
||||
return config.webuiPrefix
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { StoreApi, UseBoundStore } from 'zustand'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function randomColor() {
|
||||
const digits = '0123456789abcdef'
|
||||
let code = '#'
|
||||
for (let i = 0; i < 6; i++) {
|
||||
code += digits.charAt(Math.floor(Math.random() * 16))
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
export function errorMessage(error: any) {
|
||||
return error instanceof Error ? error.message : `${error}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a throttled function that limits how often the original function can be called
|
||||
* @param fn The function to throttle
|
||||
* @param delay The delay in milliseconds
|
||||
* @returns A throttled version of the function
|
||||
*/
|
||||
export function throttle<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
|
||||
let lastCall = 0
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
return function(this: any, ...args: Parameters<T>) {
|
||||
const now = Date.now()
|
||||
const remaining = delay - (now - lastCall)
|
||||
|
||||
if (remaining <= 0) {
|
||||
// If enough time has passed, execute the function immediately
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = null
|
||||
}
|
||||
lastCall = now
|
||||
fn.apply(this, args)
|
||||
} else if (!timeoutId) {
|
||||
// If not enough time has passed, set a timeout to execute after the remaining time
|
||||
timeoutId = setTimeout(() => {
|
||||
lastCall = Date.now()
|
||||
timeoutId = null
|
||||
fn.apply(this, args)
|
||||
}, remaining)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WithSelectors<S> = S extends { getState: () => infer T }
|
||||
? S & { use: { [K in keyof T]: () => T[K] } }
|
||||
: never
|
||||
|
||||
export const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S) => {
|
||||
const store = _store as WithSelectors<typeof _store>
|
||||
store.use = {}
|
||||
for (const k of Object.keys(store.getState())) {
|
||||
;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
Reference in New Issue
Block a user