chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:08:54 +08:00
commit 4a4a1fed67
721 changed files with 262090 additions and 0 deletions
+127
View File
@@ -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]`).'
]
+4
View File
@@ -0,0 +1,4 @@
// This file is for importing libraries that have global side effects.
// Load KaTeX mhchem extension globally
import 'katex/contrib/mhchem';
+67
View File
@@ -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/')
})
})
+49
View File
@@ -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(/\/+$/, '') + '/'
}
+40
View File
@@ -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
}
+67
View File
@@ -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
}