tr]:last:border-b-0',
45 | className
46 | )}
47 | {...props}
48 | />
49 | )
50 | }
51 |
52 | function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
53 | return (
54 |
62 | )
63 | }
64 |
65 | function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
66 | return (
67 | [role=checkbox]]:translate-y-[2px]',
71 | className
72 | )}
73 | {...props}
74 | />
75 | )
76 | }
77 |
78 | function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
79 | return (
80 | | [role=checkbox]]:translate-y-[2px]',
84 | className
85 | )}
86 | {...props}
87 | />
88 | )
89 | }
90 |
91 | function TableCaption({
92 | className,
93 | ...props
94 | }: React.ComponentProps<'caption'>) {
95 | return (
96 |
101 | )
102 | }
103 |
104 | export {
105 | Table,
106 | TableHeader,
107 | TableBody,
108 | TableFooter,
109 | TableHead,
110 | TableRow,
111 | TableCell,
112 | TableCaption,
113 | }
114 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { DayPicker } from 'react-day-picker'
3 | import { cn } from '@/lib/utils'
4 | import { buttonVariants } from '@/components/ui/button'
5 |
6 | function Calendar({
7 | className,
8 | classNames,
9 | showOutsideDays = true,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
42 | : 'aria-selected:rounded-md [&[aria-selected="true"]>button]:hover:bg-foreground [&[aria-selected="true"]>button]:hover:text-background/85'
43 | ),
44 | day_button: cn(
45 | buttonVariants({ variant: 'ghost' }),
46 | 'size-8 p-0 font-normal aria-selected:opacity-100'
47 | ),
48 | day_selected: 'opacity-100 bg-yellow-500',
49 | range_start:
50 | 'day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground',
51 | range_end:
52 | 'day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground',
53 | selected:
54 | 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
55 | today: 'bg-accent text-accent-foreground rounded-md',
56 | outside:
57 | 'day-outside text-muted-foreground aria-selected:text-muted-foreground',
58 | disabled: 'text-muted-foreground opacity-50',
59 | range_middle:
60 | 'aria-selected:bg-accent aria-selected:text-accent-foreground',
61 | hidden: 'invisible',
62 | ...classNames,
63 | }}
64 | {...props}
65 | />
66 | )
67 | }
68 |
69 | export { Calendar }
70 |
--------------------------------------------------------------------------------
/app/src/utils/llm_models.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | import hashlib
3 | import openai
4 |
5 | import requests
6 |
7 | def get_model_name_from_api(baseurl, apikey):
8 | print("Fetching available models from the API...")
9 | try:
10 | response = requests.get(
11 | f"{baseurl}/models",
12 | headers={"Authorization": f"Bearer {apikey}"},
13 | timeout=20,
14 | )
15 | if response.status_code == 200:
16 | # レスポンス形式:{"object":"list","data":[{"id":"quelmap/polaris-4b-2-grpo-h200-600step","object":"model","created":1751442481,"owned_by":"vllm","root":"quelmap/polaris-4b-2-grpo-h200-600step","parent":null,"max_model_len":30000,"permission":[{"id":"modelperm-30ab45eae2ec4f4fb2b113af00f48200","object":"model_permission","created":1751442481,"allow_create_engine":false,"allow_sampling":true,"allow_logprobs":true,"allow_search_indices":false,"allow_view":true,"allow_fine_tuning":false,"organization":"*","group":null,"is_blocking":false}]}]}
17 | models = response.json().get("data", [])
18 | if models:
19 | # 最初のモデルを選択
20 | print(f"Available models: {[model['id'] for model in models]}")
21 | return [m["id"] for m in models]
22 | else:
23 | print(f"Error fetching models: {response.status_code} - {response.text}")
24 | return ["no models available"]
25 | except requests.RequestException as e:
26 | print(f"Error connecting to the API: {str(e)}")
27 | return ["no models available"]
28 |
29 | def string_to_uuid(text: str) -> str:
30 | # return str(uuid.uuid5(uuid.NAMESPACE_DNS, text))
31 | return hashlib.md5(text.encode()).hexdigest()
32 |
33 |
34 | MODELS = []
35 | OPENAI_CLIENTS = {}
36 |
37 | def get_model_list(base_url: str , api_key: str):
38 | base_url = base_url.replace("http://localhost:", "http://host.docker.internal:")
39 | base_url = base_url.replace("/v1/", "")
40 | base_url = base_url.replace("/v1", "")
41 | base_url += "/v1"
42 | """利用可能なモデルのリストを取得"""
43 | global MODELS, OPENAI_CLIENTS
44 | # base_urlとapi_keyに基づいてモデルリストを動的に取得
45 | model_names = get_model_name_from_api(base_url, api_key)
46 | if model_names:
47 | MODELS = []
48 | OPENAI_CLIENTS = {}
49 | for model_name in model_names:
50 | model_id = model_name
51 | MODELS.append(
52 | {
53 | "id": model_id,
54 | "model_name": model_name,
55 | "base_url": base_url,
56 | "api_key": api_key,
57 | "display_name": model_name,
58 | "description": "",
59 | "config" : {}
60 | }
61 | )
62 | if api_key == "":
63 | api_key = "none"
64 | OPENAI_CLIENTS[model_id]=openai.AsyncOpenAI(base_url=base_url, api_key=api_key)
65 | return MODELS
66 | return []
67 |
68 |
69 | def get_openai_client(model_id):
70 | """指定されたモデルIDに対応するOpenAIクライアントを取得"""
71 | return OPENAI_CLIENTS.get(model_id)
72 |
73 |
74 | def get_model_by_id(model_id):
75 | """指定されたモデルIDに対応するモデルを取得"""
76 | for model in MODELS:
77 | if model["id"] == model_id:
78 | return model
79 | raise ValueError(f"Model ID {model_id} not found.")
80 |
--------------------------------------------------------------------------------
/app/src/prompts/prompt-v3.txt:
--------------------------------------------------------------------------------
1 | You are Lightning-4b, a data analysis large language model trained by quelmap. Write accurate, detailed, and comprehensive report and Python code to the Query.
2 | ### Database Structure
3 | ```
4 | @databaseinfo
5 | ```
6 |
7 | ### Output Structure
8 |
9 | You must follow a strict three-part output format using the tags ``, ``, and ``.
10 |
11 | 1. **``**: First, within the `` tags, articulate your step-by-step analysis plan. Reference the database schema to determine which tables and columns are necessary. Your plan should also anticipate potential issues during the analysis and outline your strategies to address them.
12 | 2. **``**: Next, within the `` tags, write the executable Python code for the analysis.
13 | 3. **``**: Finally, within the `` tags, generate the final analysis report. Use `{variable_name}` placeholders to embed results (such as DataFrames or Figures) from your Python code.
14 |
15 | ### Python Code Guidelines
16 |
17 | - Assume `engine` is a pre-defined database connection object.
18 | - Use `pd.read_sql_query` to read data from the database, always passing `con=engine` as an argument.
19 | - Use the `engine` object **only** for `pd.read_sql_query`. Do not call other methods on it, such as `engine.dispose()`.
20 | - If the user query specifies variable names, assume they are already defined and available in your code.
21 | - When referencing table or column names with non-standard characters (e.g., Japanese) in SQL queries, you must enclose them in double quotes (e.g., `SELECT "カラムA", "カラムB" FROM "テーブルA"`).
22 |
23 | ### Reporting Guidelines
24 |
25 | - The report must be written in Markdown.
26 | - Your report must clearly outline the analytical procedure. Detail each step of the analysis you conducted in a logical, easy-to-follow sequence, specifying the data sources and methods used.
27 | - Write the report in the same language as the user's query.
28 | - The report must be accurate and comprehensive.
29 | - **You are strictly forbidden from writing any interpretations, insights, or conclusions.**
30 |
31 | **Data & Figure Embedding**
32 | - You can directly embed `pandas.DataFrame` and `matplotlib.Figure` objects with out converting maekdown or string.
33 | - **Do not** convert `pandas.DataFrame` objects to Markdown tables or any other format. When a DataFrame is embedded, its header is included automatically and should not be written separately.
34 | - When displaying a `pandas.DataFrame`, you must show the **entire** frame, not a truncated version (e.g., `head()`).
35 | - If you perform an operation that modifies a table (e.g., adding a new column), you must include the entire, updated table in the report.
36 |
37 | **Error Handling**
38 | - Structure your main Python code using `try...except Exception as e:` blocks to manage runtime errors effectively.
39 | - Inside the except block, you must create a variable named error_message. This variable must contain a message explaining which steps were completed successfully and what caused the error. This message must clearly provide the following details:
40 | 1. Point of Failure: The specific analysis step that was in progress when the error occurred.
41 | 2. Error Type: The type of the exception (e.g., ValueError, KeyError).
42 | 3. Error Details: The detailed message from the exception object.
43 | - Regardless of whether an error occurs, It is a mandatory requirement that this error_message variable be embedded in the final report.
--------------------------------------------------------------------------------
/app/src/db_to_schema.py:
--------------------------------------------------------------------------------
1 |
2 | import pandas as pd
3 | from sqlalchemy import create_engine, inspect, text
4 |
5 | def main(engine):
6 |
7 | # テーブル名の取得
8 | with engine.connect() as connection:
9 | inspector = inspect(engine)
10 | tables = inspector.get_table_names()
11 |
12 | # 各テーブルのスキーマを取得
13 | schema_markdown = {}
14 | for table_name in tables:
15 | try:
16 | schema = save_schema_to_file(table_name, engine)
17 | if schema:
18 | schema_markdown[table_name] = schema
19 | except Exception as e:
20 | print(f"Error processing table '{table_name}': {e}")
21 |
22 | # 全てのスキーマを結合して返す
23 | if schema_markdown:
24 | # full_schema = "\n\n".join(schema_markdown)
25 | # return full_schema
26 | return schema_markdown
27 | else:
28 | return {"error": "No tables found or failed to retrieve schemas."}
29 |
30 |
31 | def save_schema_to_file(table_name,engine):
32 | """
33 | テーブルのスキーマ情報をMarkdown形式の文字列で返す
34 | ランダムサンプリングを使用してより多様な例を取得。
35 | """
36 | try:
37 |
38 | with engine.connect() as connection:
39 | inspector = inspect(engine)
40 | columns = inspector.get_columns(table_name)
41 |
42 | # テーブルの総行数を取得
43 | row_count_result = connection.execute(text(f'SELECT COUNT(*) FROM "{table_name}"'))
44 | total_rows = row_count_result.scalar()
45 |
46 | # ランダムサンプリングでデータを取得
47 | if total_rows > 100:
48 | # 大きなテーブルの場合はランダムサンプリング
49 | sample_df = pd.read_sql(text(f'SELECT * FROM "{table_name}" ORDER BY RANDOM() LIMIT 100'), connection)
50 | else:
51 | # 小さなテーブルの場合は全データを取得
52 | sample_df = pd.read_sql(text(f'SELECT * FROM "{table_name}"'), connection)
53 |
54 | markdown_lines = []
55 | markdown_lines.append(f"## Table: {table_name}\n")
56 | markdown_lines.append("| Column Name | Type | Example Value 1 | Example Value 2 | Example Value 3 |")
57 | markdown_lines.append("|---|---|---|---|---|")
58 |
59 | for col_info in columns:
60 | col_name = col_info['name']
61 | col_type = str(col_info['type'])
62 |
63 | # カラムの一意な値を取得(NaN/Nullを除外)
64 | unique_values = sample_df[col_name].dropna().drop_duplicates()
65 |
66 | # 値の分布を考慮してサンプルを選択
67 | if len(unique_values) == 0:
68 | sample1 = sample2 = sample3 = ''
69 | elif len(unique_values) == 1:
70 | sample1 = str(unique_values.iloc[0])
71 | sample2 = sample3 = ''
72 | elif len(unique_values) == 2:
73 | sample1 = str(unique_values.iloc[0])
74 | sample2 = str(unique_values.iloc[1])
75 | sample3 = ''
76 | else:
77 | # ランダムに3つ選択(重複なし)
78 | sampled_values = unique_values.sample(min(3, len(unique_values)), random_state=42)
79 | sample1 = str(sampled_values.iloc[0])
80 | sample2 = str(sampled_values.iloc[1]) if len(sampled_values) > 1 else ''
81 | sample3 = str(sampled_values.iloc[2]) if len(sampled_values) > 2 else ''
82 |
83 | markdown_lines.append(f"| {col_name} | {col_type} | {sample1} | {sample2} | {sample3} |")
84 | return "\n".join(markdown_lines)
85 |
86 | except Exception as e:
87 | print(f"Failed to save schema for table '{table_name}': {e}")
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import { AxiosError } from 'axios'
4 | import {
5 | QueryCache,
6 | QueryClient,
7 | QueryClientProvider,
8 | } from '@tanstack/react-query'
9 | import { RouterProvider, createRouter } from '@tanstack/react-router'
10 | import { toast } from 'sonner'
11 | import { useAuthStore } from '@/stores/authStore'
12 | import { handleServerError } from '@/utils/handle-server-error'
13 | import { FontProvider } from './context/font-context'
14 | import { ThemeProvider } from './context/theme-context'
15 | import { SettingsProvider } from './context/settings-context'
16 | import './index.css'
17 | // Generated Routes
18 | import { routeTree } from './routeTree.gen'
19 | import { AnalysisHistoryProvider } from './context/analysis-history-context'
20 |
21 | const queryClient = new QueryClient({
22 | defaultOptions: {
23 | queries: {
24 | retry: (failureCount, error) => {
25 | // eslint-disable-next-line no-console
26 | if (import.meta.env.DEV) console.log({ failureCount, error })
27 |
28 | if (failureCount >= 0 && import.meta.env.DEV) return false
29 | if (failureCount > 3 && import.meta.env.PROD) return false
30 |
31 | return !(
32 | error instanceof AxiosError &&
33 | [401, 403].includes(error.response?.status ?? 0)
34 | )
35 | },
36 | refetchOnWindowFocus: import.meta.env.PROD,
37 | staleTime: 10 * 1000, // 10s
38 | },
39 | mutations: {
40 | onError: (error) => {
41 | handleServerError(error)
42 |
43 | if (error instanceof AxiosError) {
44 | if (error.response?.status === 304) {
45 | toast.error('Content not modified!')
46 | }
47 | }
48 | },
49 | },
50 | },
51 | queryCache: new QueryCache({
52 | onError: (error) => {
53 | if (error instanceof AxiosError) {
54 | if (error.response?.status === 401) {
55 | toast.error('Session expired!')
56 | useAuthStore.getState().auth.reset()
57 | const redirect = `${router.history.location.href}`
58 | router.navigate({ to: '/', search: { redirect } })
59 | }
60 | if (error.response?.status === 500) {
61 | toast.error('Internal Server Error!')
62 | router.navigate({ to: '/500' })
63 | }
64 | if (error.response?.status === 403) {
65 | // router.navigate("/forbidden", { replace: true });
66 | }
67 | }
68 | },
69 | }),
70 | })
71 |
72 | // Create a new router instance
73 | const router = createRouter({
74 | routeTree,
75 | context: { queryClient },
76 | defaultPreload: 'intent',
77 | defaultPreloadStaleTime: 0,
78 | })
79 |
80 | // Register the router instance for type safety
81 | declare module '@tanstack/react-router' {
82 | interface Register {
83 | router: typeof router
84 | }
85 | }
86 |
87 | // Render the app
88 | const rootElement = document.getElementById('root')!
89 | if (!rootElement.innerHTML) {
90 | const root = ReactDOM.createRoot(rootElement)
91 | root.render(
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | )
106 | }
107 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/kibo-ui/image-zoom.tsx:
--------------------------------------------------------------------------------
1 | import Zoom, {
2 | type ControlledProps,
3 | type UncontrolledProps,
4 | } from 'react-medium-image-zoom'
5 | import { cn } from '@/lib/utils'
6 |
7 | export type ImageZoomProps = UncontrolledProps & {
8 | isZoomed?: ControlledProps['isZoomed']
9 | onZoomChange?: ControlledProps['onZoomChange']
10 | className?: string
11 | backdropClassName?: string
12 | }
13 | export const ImageZoom = ({
14 | className,
15 | backdropClassName,
16 | ...props
17 | }: ImageZoomProps) => (
18 |
34 |
48 |
49 | )
50 |
--------------------------------------------------------------------------------
/frontend/src/components/command-menu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useNavigate } from '@tanstack/react-router'
3 | import {
4 | IconArrowRightDashed,
5 | IconChevronRight,
6 | IconDeviceLaptop,
7 | IconMoon,
8 | IconSun,
9 | } from '@tabler/icons-react'
10 | import { useSearch } from '@/context/search-context'
11 | import { useTheme } from '@/context/theme-context'
12 | import { useSidebarData } from '@/hooks/use-sidebar-data'
13 | import {
14 | CommandDialog,
15 | CommandEmpty,
16 | CommandGroup,
17 | CommandInput,
18 | CommandItem,
19 | CommandList,
20 | CommandSeparator,
21 | } from '@/components/ui/command'
22 | import { ScrollArea } from './ui/scroll-area'
23 |
24 | export function CommandMenu() {
25 | const navigate = useNavigate()
26 | const { setTheme } = useTheme()
27 | const { open, setOpen } = useSearch()
28 | const { data: sidebarData } = useSidebarData()
29 |
30 | const runCommand = React.useCallback(
31 | (command: () => unknown) => {
32 | setOpen(false)
33 | command()
34 | },
35 | [setOpen]
36 | )
37 |
38 | return (
39 |
40 |
41 |
42 |
43 | No results found.
44 | {sidebarData?.navGroups.map((group) => (
45 |
46 | {group.items.map((navItem, i) => {
47 | if (navItem.url)
48 | return (
49 | {
53 | runCommand(() => navigate({ to: navItem.url }))
54 | }}
55 | >
56 |
57 |
58 |
59 | {navItem.title}
60 |
61 | )
62 |
63 | return navItem.items?.map((subItem, i) => (
64 | {
68 | runCommand(() => navigate({ to: subItem.url }))
69 | }}
70 | >
71 |
72 |
73 |
74 | {navItem.title} {subItem.title}
75 |
76 | ))
77 | })}
78 |
79 | ))}
80 |
81 |
82 | runCommand(() => setTheme('light'))}>
83 | Light
84 |
85 | runCommand(() => setTheme('dark'))}>
86 |
87 | Dark
88 |
89 | runCommand(() => setTheme('system'))}>
90 |
91 | System
92 |
93 |
94 |
95 |
96 |
97 | )
98 | }
99 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "quelmap-analysis",
3 | "private": false,
4 | "version": "1.4.1",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "lint:fix": "eslint . --fix",
11 | "preview": "vite preview",
12 | "format:check": "prettier --check .",
13 | "format": "prettier --write .",
14 | "knip": "knip"
15 | },
16 | "dependencies": {
17 | "@clerk/clerk-react": "^5.32.1",
18 | "@hookform/resolvers": "^5.1.1",
19 | "@icons-pack/react-simple-icons": "^13.5.0",
20 | "@radix-ui/react-alert-dialog": "^1.1.14",
21 | "@radix-ui/react-avatar": "^1.1.10",
22 | "@radix-ui/react-checkbox": "^1.3.2",
23 | "@radix-ui/react-collapsible": "^1.1.11",
24 | "@radix-ui/react-dialog": "^1.1.14",
25 | "@radix-ui/react-dropdown-menu": "^2.1.15",
26 | "@radix-ui/react-icons": "^1.3.2",
27 | "@radix-ui/react-label": "^2.1.7",
28 | "@radix-ui/react-popover": "^1.1.14",
29 | "@radix-ui/react-progress": "^1.1.7",
30 | "@radix-ui/react-radio-group": "^1.3.7",
31 | "@radix-ui/react-scroll-area": "^1.2.9",
32 | "@radix-ui/react-select": "^2.2.5",
33 | "@radix-ui/react-separator": "^1.1.7",
34 | "@radix-ui/react-slot": "^1.2.3",
35 | "@radix-ui/react-switch": "^1.2.5",
36 | "@radix-ui/react-tabs": "^1.1.12",
37 | "@radix-ui/react-tooltip": "^1.2.7",
38 | "@react-pdf/renderer": "^4.3.0",
39 | "@shikijs/transformers": "^3.8.0",
40 | "@tabler/icons-react": "^3.34.0",
41 | "@tailwindcss/vite": "^4.1.10",
42 | "@tanstack/react-query": "^5.81.2",
43 | "@tanstack/react-router": "^1.121.34",
44 | "@tanstack/react-table": "^8.21.3",
45 | "axios": "^1.10.0",
46 | "class-variance-authority": "^0.7.1",
47 | "clsx": "^2.1.1",
48 | "cmdk": "1.1.1",
49 | "date-fns": "^4.1.0",
50 | "gsap": "^3.13.0",
51 | "i": "^0.3.7",
52 | "input-otp": "^1.4.2",
53 | "js-cookie": "^3.0.5",
54 | "lucide-react": "^0.523.0",
55 | "motion": "^12.23.5",
56 | "npm": "^11.4.2",
57 | "react": "^19.1.0",
58 | "react-day-picker": "9.7.0",
59 | "react-dom": "^19.1.0",
60 | "react-dropzone": "^14.3.8",
61 | "react-hook-form": "^7.61.1",
62 | "react-markdown": "^10.1.0",
63 | "react-medium-image-zoom": "^5.2.14",
64 | "react-split": "^2.0.14",
65 | "react-spreadsheet": "^0.10.1",
66 | "react-to-print": "^3.1.1",
67 | "react-top-loading-bar": "^3.0.2",
68 | "recharts": "^3.0.0",
69 | "remark-gfm": "^4.0.1",
70 | "shiki": "^3.8.0",
71 | "sonner": "^2.0.5",
72 | "tailwind-merge": "^3.3.1",
73 | "tailwindcss": "^4.1.10",
74 | "tw-animate-css": "^1.3.4",
75 | "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz",
76 | "zod": "^3.25.67",
77 | "zustand": "^5.0.5"
78 | },
79 | "devDependencies": {
80 | "@eslint/js": "^9.29.0",
81 | "@faker-js/faker": "^9.8.0",
82 | "@tanstack/eslint-plugin-query": "^5.81.2",
83 | "@tanstack/react-query-devtools": "^5.81.2",
84 | "@tanstack/react-router-devtools": "^1.121.34",
85 | "@tanstack/router-plugin": "^1.121.34",
86 | "@trivago/prettier-plugin-sort-imports": "^5.2.2",
87 | "@types/js-cookie": "^3.0.6",
88 | "@types/node": "^24.0.4",
89 | "@types/react": "^19.1.8",
90 | "@types/react-dom": "^19.1.6",
91 | "@vitejs/plugin-react-swc": "^3.10.2",
92 | "eslint": "^9.29.0",
93 | "eslint-plugin-react-hooks": "^5.2.0",
94 | "eslint-plugin-react-refresh": "^0.4.20",
95 | "globals": "^16.2.0",
96 | "knip": "^5.61.2",
97 | "prettier": "^3.6.0",
98 | "prettier-plugin-tailwindcss": "^0.6.13",
99 | "typescript": "~5.8.3",
100 | "typescript-eslint": "^8.35.0",
101 | "vite": "^7.0.0"
102 | },
103 | "overrides": {
104 | "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/frontend/src/hooks/use-table-data.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | useQuery,
4 | useInfiniteQuery,
5 | useMutation,
6 | useQueryClient,
7 | } from '@tanstack/react-query'
8 |
9 | const API_BASE_URL = import.meta.env.VITE_SERVER_URL || 'http://localhost:8000'
10 |
11 | export interface TableDataResponse {
12 | table_name: string
13 | columns: string[]
14 | data: Record[]
15 | total_rows: number
16 | preview_rows: number
17 | }
18 |
19 | export interface TableDataParams {
20 | limit?: number
21 | offset?: number
22 | sort_column?: string
23 | sort_direction?: 'asc' | 'desc'
24 | filter_column?: string
25 | filter_value?: string
26 | }
27 |
28 | const fetchTableData = async (
29 | tableName: string,
30 | params: TableDataParams = {}
31 | ): Promise => {
32 | const response = await axios.get(
33 | `${API_BASE_URL}/api/table-data/${tableName}`,
34 | {
35 | params,
36 | }
37 | )
38 | return response.data
39 | }
40 |
41 | // テーブル削除API
42 | const deleteTable = async (tableName: string): Promise => {
43 | const response = await axios.delete(
44 | `${API_BASE_URL}/api/delete-table/${tableName}`
45 | )
46 | console.log('削除API レスポンス:', response.data)
47 | return response.data
48 | }
49 |
50 | // テーブル名変更API
51 | const renameTable = async (
52 | tableName: string,
53 | newTableName: string
54 | ): Promise => {
55 | const formData = new FormData()
56 | formData.append('new_table_name', newTableName)
57 | const response = await axios.put(
58 | `${API_BASE_URL}/api/rename-table/${tableName}`,
59 | formData
60 | )
61 | return response.data
62 | }
63 |
64 | export const useTableData = (
65 | tableName: string,
66 | params: TableDataParams = {}
67 | ) => {
68 | return useQuery({
69 | queryKey: ['tableData', tableName, params],
70 | queryFn: () => fetchTableData(tableName, params),
71 | staleTime: 2 * 60 * 1000, // 2分間キャッシュ
72 | refetchOnWindowFocus: false,
73 | enabled: !!tableName, // tableNameが存在する場合のみクエリを実行
74 | })
75 | }
76 |
77 | // 無限スクロール用のフック
78 | export const useInfiniteTableData = (
79 | tableName: string,
80 | baseParams: Omit = {}
81 | ) => {
82 | return useInfiniteQuery({
83 | queryKey: ['infiniteTableData', tableName, baseParams],
84 | queryFn: ({ pageParam = 0 }) =>
85 | fetchTableData(tableName, { ...baseParams, offset: pageParam }),
86 | getNextPageParam: (lastPage, allPages) => {
87 | // 次のページがあるかチェック
88 | const totalFetched = allPages.reduce(
89 | (sum, page) => sum + page.preview_rows,
90 | 0
91 | )
92 | return totalFetched < lastPage.total_rows ? totalFetched : undefined
93 | },
94 | staleTime: 2 * 60 * 1000,
95 | refetchOnWindowFocus: false,
96 | enabled: !!tableName,
97 | initialPageParam: 0,
98 | })
99 | }
100 |
101 | // テーブル削除用のフック
102 | export const useDeleteTable = () => {
103 | const queryClient = useQueryClient()
104 |
105 | return useMutation({
106 | mutationFn: deleteTable,
107 | onSuccess: (_, tableName: string) => {
108 | // テーブルリストのキャッシュを無効化して再取得
109 | queryClient.invalidateQueries({ queryKey: ['tableList'] })
110 | // 削除されたテーブルのデータキャッシュも削除
111 | queryClient.removeQueries({ queryKey: ['tableData', tableName] })
112 | queryClient.removeQueries({ queryKey: ['infiniteTableData', tableName] })
113 | },
114 | })
115 | }
116 |
117 | // テーブル名変更用のフック
118 | export const useRenameTable = (onError: (error: any) => void) => {
119 | const queryClient = useQueryClient()
120 |
121 | return useMutation({
122 | mutationFn: ({
123 | tableName,
124 | newTableName,
125 | }: {
126 | tableName: string
127 | newTableName: string
128 | }) => renameTable(tableName, newTableName),
129 | onSuccess: (_, { tableName }) => {
130 | // テーブルリストのキャッシュを無効化して再取得
131 | queryClient.invalidateQueries({ queryKey: ['tableList'] })
132 | // 古いテーブル名のキャッシュを削除
133 | queryClient.removeQueries({ queryKey: ['tableData', tableName] })
134 | queryClient.removeQueries({ queryKey: ['infiniteTableData', tableName] })
135 | },
136 | onError: onError,
137 | })
138 | }
139 |
--------------------------------------------------------------------------------
/frontend/src/features/analysis-report/components/followup-input.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AIInput,
3 | AIInputModelSelect,
4 | AIInputModelSelectContent,
5 | AIInputModelSelectItem,
6 | AIInputModelSelectTrigger,
7 | AIInputModelSelectValue,
8 | AIInputSubmit,
9 | AIInputTextarea,
10 | AIInputToolbar,
11 | AIInputTools,
12 | AIInputMultiSelectTable
13 | } from '@/components/ui/kibo-ui/ai-input'
14 | import { type FormEventHandler } from 'react'
15 | import type { ModelInfo } from '@/hooks/use-analysis'
16 |
17 | // プレゼンテーション専用コンポーネント
18 | export interface FollowupInputProps {
19 | text: string
20 | onTextChange: (text: string) => void
21 | model: string
22 | onModelChange: (model: string) => void
23 | models: ModelInfo[]
24 | agenticMode: boolean
25 | onAgenticModeChange: (checked: boolean) => void
26 | selectedTables: string[]
27 | onSelectedTablesChange: (tables: string[]) => void
28 | tables: { name: string }[] | undefined
29 | tablesError?: unknown
30 | status: 'submitted' | 'streaming' | 'ready' | 'error'
31 | isprocessing?: boolean
32 | onSubmit: (data: { text: string }) => void
33 | }
34 |
35 | export default function FollowupInput({
36 | text,
37 | onTextChange,
38 | model,
39 | onModelChange,
40 | models,
41 | selectedTables,
42 | onSelectedTablesChange,
43 | tables,
44 | tablesError,
45 | status,
46 | isprocessing,
47 | onSubmit
48 | }: FollowupInputProps) {
49 | const handleSubmit: FormEventHandler = (e) => {
50 | e.preventDefault()
51 | if (!text.trim()) return
52 | onSubmit({ text: text.trim() })
53 | }
54 |
55 | const disabled = !text || !model || !tables || tables.length === 0 || !!tablesError || isprocessing
56 |
57 | return (
58 |
59 |
60 |
61 | onTextChange(e.target.value)}
64 | value={text}
65 | />
66 |
67 |
68 |
69 |
70 |
71 | {model && models?.find((m) => m.id === model)?.name}
72 |
73 |
74 |
75 | {models?.map((m) => (
76 |
77 |
78 | {m.name}
79 | {m.description}
80 |
81 |
82 | ))}
83 |
84 |
85 | ({ value: t.name, label: t.name }))}
87 | selected={selectedTables}
88 | onSelectedChange={onSelectedTablesChange}
89 | placeholder='Select Tables'
90 | />
91 |
92 |
93 |
94 |
95 |
96 |
97 | )
98 | }
99 |
--------------------------------------------------------------------------------
/frontend/src/context/analysis-history-context.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'
2 |
3 | interface AnalysisHistoryContextType {
4 | history: AnalysisHistoryItem[]
5 | addToHistory: (id: string, query: string) => void
6 | removeFromHistory: (id: string) => void
7 | clearHistory: () => void
8 | setItemLoading: (id: string, isLoading: boolean) => void
9 | }
10 |
11 | const AnalysisHistoryCtx = createContext(null)
12 |
13 | export const AnalysisHistoryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
14 | const value = useAnalysisHistory()
15 | return {children}
16 | }
17 | /**
18 | * `history`を全域範囲に統一する.
19 | */
20 | export const useSharedAnalysisHistory = () => {
21 | const ctx = useContext(AnalysisHistoryCtx)
22 | if (!ctx) throw new Error('useSharedAnalysisHistory must be used within Provider')
23 | return ctx
24 | }
25 |
26 | export interface AnalysisHistoryItem {
27 | id: string
28 | query: string
29 | timestamp: number
30 | isLoading: boolean
31 | }
32 |
33 | const STORAGE_KEY = 'analysis_history'
34 | const MAX_HISTORY_ITEMS = 1000
35 |
36 | const useAnalysisHistory = () => {
37 | const [history, setHistory] = useState([])
38 |
39 | // ローカルストレージから履歴を読み込み
40 | useEffect(() => {
41 | try {
42 | const stored = localStorage.getItem(STORAGE_KEY)
43 | if (stored) {
44 | const parsedHistory = JSON.parse(stored) as AnalysisHistoryItem[]
45 | setHistory(parsedHistory)
46 | }
47 | } catch (error) {
48 | console.error('Failed to load analysis history:', error)
49 | }
50 | }, [])
51 |
52 | // 履歴をローカルストレージに保存
53 | const saveToStorage = useCallback((newHistory: AnalysisHistoryItem[]) => {
54 | try {
55 | localStorage.setItem(STORAGE_KEY, JSON.stringify(newHistory))
56 | } catch (error) {
57 | console.error('Failed to save analysis history:', error)
58 | }
59 | }, [])
60 |
61 | // 新しい分析を履歴に追加
62 | const addToHistory = useCallback((id: string, query: string) => {
63 | const newItem: AnalysisHistoryItem = {
64 | id,
65 | query: query.length > 50 ? query.substring(0, 50) + '...' : query,
66 | timestamp: Date.now(),
67 | isLoading: true
68 | }
69 |
70 | setHistory((prevHistory) => {
71 | // 同じIDがある場合は削除(重複を避ける)
72 | const filteredHistory = prevHistory.filter((item) => item.id !== id)
73 |
74 | // 新しいアイテムを先頭に追加し、最大数を超えたら古いものを削除
75 | const newHistory = [newItem, ...filteredHistory].slice(
76 | 0,
77 | MAX_HISTORY_ITEMS
78 | )
79 |
80 | // 先にストレージに保存
81 | saveToStorage(newHistory)
82 |
83 | // デバッグ用のログ
84 | console.log('Analysis history updated:', {
85 | newItem,
86 | previousCount: prevHistory.length,
87 | newCount: newHistory.length,
88 | })
89 |
90 | return newHistory
91 | })
92 |
93 | }, [saveToStorage])
94 |
95 | // 履歴から特定のアイテムを削除
96 | const removeFromHistory = useCallback(
97 | (id: string) => {
98 | setHistory((prevHistory) => {
99 | const newHistory = prevHistory.filter((item) => item.id !== id)
100 | saveToStorage(newHistory)
101 | return newHistory
102 | })
103 | },
104 | [saveToStorage]
105 | )
106 |
107 | // 履歴をクリア
108 | const clearHistory = useCallback(() => {
109 | setHistory([])
110 | try {
111 | localStorage.removeItem(STORAGE_KEY)
112 | } catch (error) {
113 | console.error('Failed to clear analysis history:', error)
114 | }
115 | }, [saveToStorage])
116 |
117 | // ローダーアイコン制御するbooleanの値を更新
118 | const setItemLoading = useCallback((id: string, isLoading: boolean) => {
119 | setHistory(prev => {
120 | const newHistory = prev.map(item =>
121 | item.id === id
122 | ? {...item, isLoading: isLoading}
123 | : item
124 | )
125 | saveToStorage(newHistory)
126 | return newHistory
127 | })
128 | }, [saveToStorage])
129 |
130 | return {
131 | history,
132 | setItemLoading,
133 | addToHistory,
134 | removeFromHistory,
135 | clearHistory,
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/app/src/code_service.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import base64
4 | import httpx
5 | from .models.requests import VariableRetrievalResponse
6 |
7 | CODE_RUNNER_URL = os.getenv("CODE_RUNNER_URL")
8 |
9 | class CodeService:
10 | async def code_execution(self, python_code: str, access_id: str):
11 | """Pythonコードを実行する関数"""
12 | try:
13 | async with httpx.AsyncClient(timeout=30.0) as client:
14 | response = await client.post(
15 | CODE_RUNNER_URL + "code",
16 | json={
17 | "code": python_code.replace("\\", "%@"),
18 | "id": access_id,
19 | }
20 | )
21 |
22 | response.raise_for_status() # HTTPエラーなら例外を投げる
23 |
24 | # --- code-runner からのレスポンスを処理 ---
25 | runner_result = response.json()
26 |
27 | if "error" in runner_result:
28 | print(f"Error from code runner: {runner_result['error']}")
29 | return {"code_error": runner_result['error']}
30 |
31 | print("Code executed successfully:")
32 | return {"result": runner_result}
33 |
34 | except httpx.HTTPStatusError as e:
35 | print(f"HTTP connection error during code execution: {e}")
36 | return {"error": "HTTP connection error during code execution"}
37 | except httpx.TimeoutException:
38 | print("Request timed out error during code execution")
39 | return {"error": "Request timed out error during code execution"}
40 | except Exception as e:
41 | print(f"An unexpected error occurred: {e}")
42 | return {"error": "An unexpected error during code execution"}
43 |
44 | async def code_rollback(self, access_id: str):
45 | """コードのロールバックを行う関数"""
46 | try:
47 | async with httpx.AsyncClient(timeout=30.0) as client:
48 | response = await client.post(
49 | CODE_RUNNER_URL + "rollback",
50 | json={"id": access_id}
51 | )
52 |
53 | response.raise_for_status() # HTTPエラーなら例外を投げる
54 |
55 | rollback_result = response.json()
56 | if "error" in rollback_result:
57 | print(f"Error during rollback: {rollback_result['error']}")
58 | return {"error": rollback_result['error']}
59 |
60 | print("Rollback executed successfully")
61 | return {"result": rollback_result}
62 |
63 | except httpx.HTTPStatusError as e:
64 | print(f"HTTP connection error during rollback: {e}")
65 | return {"error": "HTTP connection error during rollback"}
66 | except httpx.TimeoutException:
67 | print("Request timed out error during rollback")
68 | return {"error": "Request timed out error during rollback"}
69 | except Exception as e:
70 | print(f"An unexpected error occurred during rollback: {e}")
71 | return {"error": "An unexpected error during rollback"}
72 |
73 | async def get_variable(self, request: VariableRetrievalResponse):
74 | """変数を取得するエンドポイント"""
75 | endpoint_url = f"{CODE_RUNNER_URL}var"
76 | try:
77 | async with httpx.AsyncClient() as client:
78 | response = await client.post(endpoint_url, json={"id": request.id, "name": request.name})
79 |
80 | if response.status_code != 200:
81 | return {"error": f"Code runner error: {response.status_code}", "detail": response.text}
82 |
83 | return response.json()
84 | except httpx.RequestError as e:
85 | return {"error": "Request failed", "detail": str(e)}
86 |
87 | async def get_variable_value(self, analysis_id: str, variable_name: str):
88 | """分析マネージャー用の変数取得メソッド"""
89 | endpoint_url = f"{CODE_RUNNER_URL}var"
90 | try:
91 | async with httpx.AsyncClient() as client:
92 | response = await client.post(endpoint_url, json={"id": analysis_id, "name": variable_name})
93 |
94 | if response.status_code != 200:
95 | return None
96 |
97 | return response.json()
98 | except Exception as e:
99 | print(f"Error getting variable {variable_name}: {e}")
100 | return None
--------------------------------------------------------------------------------
/frontend/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 | import { XIcon } from 'lucide-react'
6 | import { cn } from '@/lib/utils'
7 |
8 | function Dialog({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function DialogTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return
18 | }
19 |
20 | function DialogPortal({
21 | ...props
22 | }: React.ComponentProps) {
23 | return
24 | }
25 |
26 | function DialogClose({
27 | ...props
28 | }: React.ComponentProps) {
29 | return
30 | }
31 |
32 | function DialogOverlay({
33 | className,
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
45 | )
46 | }
47 |
48 | function DialogContent({
49 | className,
50 | children,
51 | ...props
52 | }: React.ComponentProps) {
53 | return (
54 |
55 |
56 |
64 | {children}
65 |
66 |
67 | Close
68 |
69 |
70 |
71 | )
72 | }
73 |
74 | function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
85 | return (
86 |
94 | )
95 | }
96 |
97 | function DialogTitle({
98 | className,
99 | ...props
100 | }: React.ComponentProps) {
101 | return (
102 |
107 | )
108 | }
109 |
110 | function DialogDescription({
111 | className,
112 | ...props
113 | }: React.ComponentProps) {
114 | return (
115 |
120 | )
121 | }
122 |
123 | export {
124 | Dialog,
125 | DialogClose,
126 | DialogContent,
127 | DialogDescription,
128 | DialogFooter,
129 | DialogHeader,
130 | DialogOverlay,
131 | DialogPortal,
132 | DialogTitle,
133 | DialogTrigger,
134 | }
135 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {
3 | Controller,
4 | FormProvider,
5 | useFormContext,
6 | useFormState,
7 | type ControllerProps,
8 | type FieldPath,
9 | type FieldValues,
10 | } from 'react-hook-form'
11 | import * as LabelPrimitive from '@radix-ui/react-label'
12 | import { Slot } from '@radix-ui/react-slot'
13 | import { cn } from '@/lib/utils'
14 | import { Label } from '@/components/ui/label'
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath,
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath,
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState } = useFormContext()
46 | const formState = useFormState({ name: fieldContext.name })
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error('useFormField should be used within ')
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
74 | const id = React.useId()
75 |
76 | return (
77 |
78 |
83 |
84 | )
85 | }
86 |
87 | function FormLabel({
88 | className,
89 | ...props
90 | }: React.ComponentProps) {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
101 | )
102 | }
103 |
104 | function FormControl({ ...props }: React.ComponentProps) {
105 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
106 |
107 | return (
108 |
119 | )
120 | }
121 |
122 | function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
123 | const { formDescriptionId } = useFormField()
124 |
125 | return (
126 |
132 | )
133 | }
134 |
135 | function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
136 | const { error, formMessageId } = useFormField()
137 | const body = error ? String(error?.message ?? '') : props.children
138 |
139 | if (!body) {
140 | return null
141 | }
142 |
143 | return (
144 |
150 | {body}
151 |
152 | )
153 | }
154 |
155 | export {
156 | useFormField,
157 | Form,
158 | FormItem,
159 | FormLabel,
160 | FormControl,
161 | FormDescription,
162 | FormMessage,
163 | FormField,
164 | }
165 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
3 | import { cn } from '@/lib/utils'
4 | import { buttonVariants } from '@/components/ui/button'
5 |
6 | function AlertDialog({
7 | ...props
8 | }: React.ComponentProps) {
9 | return
10 | }
11 |
12 | function AlertDialogTrigger({
13 | ...props
14 | }: React.ComponentProps) {
15 | return (
16 |
17 | )
18 | }
19 |
20 | function AlertDialogPortal({
21 | ...props
22 | }: React.ComponentProps) {
23 | return (
24 |
25 | )
26 | }
27 |
28 | function AlertDialogOverlay({
29 | className,
30 | ...props
31 | }: React.ComponentProps) {
32 | return (
33 |
41 | )
42 | }
43 |
44 | function AlertDialogContent({
45 | className,
46 | ...props
47 | }: React.ComponentProps) {
48 | return (
49 |
50 |
51 |
59 |
60 | )
61 | }
62 |
63 | function AlertDialogHeader({
64 | className,
65 | ...props
66 | }: React.ComponentProps<'div'>) {
67 | return (
68 |
73 | )
74 | }
75 |
76 | function AlertDialogFooter({
77 | className,
78 | ...props
79 | }: React.ComponentProps<'div'>) {
80 | return (
81 |
89 | )
90 | }
91 |
92 | function AlertDialogTitle({
93 | className,
94 | ...props
95 | }: React.ComponentProps) {
96 | return (
97 |
102 | )
103 | }
104 |
105 | function AlertDialogDescription({
106 | className,
107 | ...props
108 | }: React.ComponentProps) {
109 | return (
110 |
115 | )
116 | }
117 |
118 | function AlertDialogAction({
119 | className,
120 | ...props
121 | }: React.ComponentProps) {
122 | return (
123 |
127 | )
128 | }
129 |
130 | function AlertDialogCancel({
131 | className,
132 | ...props
133 | }: React.ComponentProps) {
134 | return (
135 |
139 | )
140 | }
141 |
142 | export {
143 | AlertDialog,
144 | AlertDialogPortal,
145 | AlertDialogOverlay,
146 | AlertDialogTrigger,
147 | AlertDialogContent,
148 | AlertDialogHeader,
149 | AlertDialogFooter,
150 | AlertDialogTitle,
151 | AlertDialogDescription,
152 | AlertDialogAction,
153 | AlertDialogCancel,
154 | }
155 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as SheetPrimitive from '@radix-ui/react-dialog'
3 | import { XIcon } from 'lucide-react'
4 | import { cn } from '@/lib/utils'
5 |
6 | function Sheet({ ...props }: React.ComponentProps) {
7 | return
8 | }
9 |
10 | function SheetTrigger({
11 | ...props
12 | }: React.ComponentProps) {
13 | return
14 | }
15 |
16 | function SheetClose({
17 | ...props
18 | }: React.ComponentProps) {
19 | return
20 | }
21 |
22 | function SheetPortal({
23 | ...props
24 | }: React.ComponentProps) {
25 | return
26 | }
27 |
28 | function SheetOverlay({
29 | className,
30 | ...props
31 | }: React.ComponentProps) {
32 | return (
33 |
41 | )
42 | }
43 |
44 | function SheetContent({
45 | className,
46 | children,
47 | side = 'right',
48 | ...props
49 | }: React.ComponentProps & {
50 | side?: 'top' | 'right' | 'bottom' | 'left'
51 | }) {
52 | return (
53 |
54 |
55 |
71 | {children}
72 |
73 |
74 | Close
75 |
76 |
77 |
78 | )
79 | }
80 |
81 | function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
82 | return (
83 |
88 | )
89 | }
90 |
91 | function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
92 | return (
93 |
98 | )
99 | }
100 |
101 | function SheetTitle({
102 | className,
103 | ...props
104 | }: React.ComponentProps) {
105 | return (
106 |
111 | )
112 | }
113 |
114 | function SheetDescription({
115 | className,
116 | ...props
117 | }: React.ComponentProps) {
118 | return (
119 |
124 | )
125 | }
126 |
127 | export {
128 | Sheet,
129 | SheetTrigger,
130 | SheetClose,
131 | SheetContent,
132 | SheetHeader,
133 | SheetFooter,
134 | SheetTitle,
135 | SheetDescription,
136 | }
137 |
--------------------------------------------------------------------------------
/app/src/prompts/prompt-v3+.txt:
--------------------------------------------------------------------------------
1 | Your job is Write accurate, detailed, and comprehensive report and Python code to the Query.
2 | ### Database Structure
3 | ```
4 | @databaseinfo
5 | ```
6 |
7 | ### Output Structure
8 |
9 | You must follow a strict three-part output format using the tags ``, ``, and ``.
10 |
11 | 1. **``**: First, within the `` tags, articulate your step-by-step analysis plan. Reference the database schema to determine which tables and columns are necessary. Your plan should also anticipate potential issues during the analysis and outline your strategies to address them.
12 | 2. **``**: Next, within the `` tags, write the executable Python code for the analysis.
13 | 3. **``**: Finally, within the `` tags, generate the final analysis report. Use `{variable_name}` placeholders to embed results (such as DataFrames or Figures) from your Python code.
14 |
15 | ### Python Code Guidelines
16 |
17 | - Assume `engine` is a pre-defined database connection object.
18 | - Use `pd.read_sql_query` to read data from the database, always passing `con=engine` as an argument.
19 | - Use the `engine` object **only** for `pd.read_sql_query`. Do not call other methods on it, such as `engine.dispose()`.
20 | - If the user query specifies variable names, assume they are already defined and available in your code.
21 | - When referencing table or column names with non-standard characters (e.g., Japanese) in SQL queries, you must enclose them in double quotes (e.g., `SELECT "カラムA", "カラムB" FROM "テーブルA"`).
22 |
23 | ### Reporting Guidelines
24 |
25 | - The report must be written in Markdown.
26 | - Your report must clearly outline the analytical procedure. Detail each step of the analysis you conducted in a logical, easy-to-follow sequence, specifying the data sources and methods used.
27 | - Write the report in the same language as the user's query.
28 | - The report must be accurate and comprehensive.
29 | - **You are strictly forbidden from writing any interpretations, insights, or conclusions.**
30 |
31 | **Data & Figure Embedding**
32 | - You can directly embed `pandas.DataFrame` and `matplotlib.Figure` objects with out converting maekdown or string.
33 | - **Do not** convert `pandas.DataFrame` objects to Markdown tables or any other format. When a DataFrame is embedded, its header is included automatically and should not be written separately.
34 | - When displaying a `pandas.DataFrame`, you must show the **entire** frame, not a truncated version (e.g., `head()`).
35 | - If you perform an operation that modifies a table (e.g., adding a new column), you must include the entire, updated table in the report.
36 |
37 | **Error Handling**
38 | - Structure your main Python code using `try...except Exception as e:` blocks to manage runtime errors effectively.
39 | - Inside the except block, you must create a variable named error_message. This variable must contain a message explaining which steps were completed successfully and what caused the error. This message must clearly provide the following details:
40 | 1. Point of Failure: The specific analysis step that was in progress when the error occurred.
41 | 2. Error Type: The type of the exception (e.g., ValueError, KeyError).
42 | 3. Error Details: The detailed message from the exception object.
43 | - Regardless of whether an error occurs, It is a mandatory requirement that this error_message variable be embedded in the final report.
44 |
45 | **Example Output (partially omitted)**
46 |
47 | First, I clarify the user query. They want to "classify by product type and aggregate the counts."
48 | Here, "product type" refers to the category each product...
49 | (omitted)
50 |
51 |
52 | import pandas as pd
53 | import matplotlib.pyplot as plt
54 | df_product_type = pd.read_sql_query("SELECT product_id, product_type FROM product", con=engine)
55 | df_type_count = (df_product_type.drop_duplicates(subset="product_id")
56 | .groupby("product_type")
57 | .size()
58 | .reset_index(name="count"))
59 |
60 | fig = plt.figure(figsize=(8, 6))
61 | plt.bar(df_type_count["product_type"], df_type_count["count"], color="skyblue")
62 | plt.xticks(rotation=45)
63 | plt.xlabel("product Type")
64 | plt.ylabel("Count")
65 | plt.title("Count of Each product Type")
66 | (省略)
67 |
68 |
69 | ## Analysis Procedure
70 | 1. Extracted product_id and product_type from the product table.
71 | 2. Verified data presence and obtained {df_product_type.shape[0]} rows.
72 | 3. Normalized product_type by replacing NULL with empty strings and trimming surrounding whitespace.
73 | 4. Removed duplicate product_id values and aggregated unique product_id counts per product_type.
74 | ## Analysis Results
75 | Number of products per product_type
76 | {df_type_count}
77 | Figure:
78 | {fig}
79 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/reactbits/split-text.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from 'react'
2 | import { gsap } from 'gsap'
3 | import { ScrollTrigger } from 'gsap/ScrollTrigger'
4 | import { SplitText as GSAPSplitText } from 'gsap/SplitText'
5 |
6 | gsap.registerPlugin(ScrollTrigger, GSAPSplitText)
7 |
8 | export interface SplitTextProps {
9 | text: string
10 | className?: string
11 | delay?: number
12 | duration?: number
13 | ease?: string | ((t: number) => number)
14 | splitType?: 'chars' | 'words' | 'lines' | 'words, chars'
15 | from?: gsap.TweenVars
16 | to?: gsap.TweenVars
17 | threshold?: number
18 | rootMargin?: string
19 | textAlign?: React.CSSProperties['textAlign']
20 | onLetterAnimationComplete?: () => void
21 | }
22 |
23 | const SplitText: React.FC = ({
24 | text,
25 | className = '',
26 | delay = 100,
27 | duration = 0.6,
28 | ease = 'power3.out',
29 | splitType = 'chars',
30 | from = { opacity: 0, y: 40 },
31 | to = { opacity: 1, y: 0 },
32 | threshold = 0.1,
33 | rootMargin = '-100px',
34 | textAlign = 'center',
35 | onLetterAnimationComplete,
36 | }) => {
37 | const ref = useRef(null)
38 | const animationCompletedRef = useRef(false)
39 | const scrollTriggerRef = useRef(null)
40 |
41 | useEffect(() => {
42 | if (typeof window === 'undefined' || !ref.current || !text) return
43 |
44 | const el = ref.current
45 |
46 | animationCompletedRef.current = false
47 |
48 | const absoluteLines = splitType === 'lines'
49 | if (absoluteLines) el.style.position = 'relative'
50 |
51 | let splitter: GSAPSplitText
52 | try {
53 | splitter = new GSAPSplitText(el, {
54 | type: splitType,
55 | absolute: absoluteLines,
56 | linesClass: 'split-line',
57 | })
58 | } catch (error) {
59 | console.error('Failed to create SplitText:', error)
60 | return
61 | }
62 |
63 | let targets: Element[]
64 | switch (splitType) {
65 | case 'lines':
66 | targets = splitter.lines
67 | break
68 | case 'words':
69 | targets = splitter.words
70 | break
71 | case 'chars':
72 | targets = splitter.chars
73 | break
74 | default:
75 | targets = splitter.chars
76 | }
77 |
78 | if (!targets || targets.length === 0) {
79 | console.warn('No targets found for SplitText animation')
80 | splitter.revert()
81 | return
82 | }
83 |
84 | targets.forEach((t) => {
85 | ;(t as HTMLElement).style.willChange = 'transform, opacity'
86 | })
87 |
88 | const startPct = (1 - threshold) * 100
89 | const marginMatch = /^(-?\d+(?:\.\d+)?)(px|em|rem|%)?$/.exec(rootMargin)
90 | const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0
91 | const marginUnit = marginMatch ? marginMatch[2] || 'px' : 'px'
92 | const sign =
93 | marginValue < 0
94 | ? `-=${Math.abs(marginValue)}${marginUnit}`
95 | : `+=${marginValue}${marginUnit}`
96 | const start = `top ${startPct}%${sign}`
97 |
98 | const tl = gsap.timeline({
99 | scrollTrigger: {
100 | trigger: el,
101 | start,
102 | toggleActions: 'play none none none',
103 | once: true,
104 | onToggle: (self) => {
105 | scrollTriggerRef.current = self
106 | },
107 | },
108 | smoothChildTiming: true,
109 | onComplete: () => {
110 | animationCompletedRef.current = true
111 | gsap.set(targets, {
112 | ...to,
113 | clearProps: 'willChange',
114 | immediateRender: true,
115 | })
116 | onLetterAnimationComplete?.()
117 | },
118 | })
119 |
120 | tl.set(targets, { ...from, immediateRender: false, force3D: true })
121 | tl.to(targets, {
122 | ...to,
123 | duration,
124 | ease,
125 | stagger: delay / 1000,
126 | force3D: true,
127 | })
128 |
129 | return () => {
130 | tl.kill()
131 | if (scrollTriggerRef.current) {
132 | scrollTriggerRef.current.kill()
133 | scrollTriggerRef.current = null
134 | }
135 | gsap.killTweensOf(targets)
136 | if (splitter) {
137 | splitter.revert()
138 | }
139 | }
140 | }, [
141 | text,
142 | delay,
143 | duration,
144 | ease,
145 | splitType,
146 | from,
147 | to,
148 | threshold,
149 | rootMargin,
150 | onLetterAnimationComplete,
151 | ])
152 |
153 | return (
154 |
162 | {text}
163 |
164 | )
165 | }
166 |
167 | export default SplitText
168 |
--------------------------------------------------------------------------------
/frontend/src/hooks/use-analysis.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { useMutation, useQuery } from '@tanstack/react-query'
3 | import { useSettings } from '@/context/settings-context'
4 |
5 | const API_BASE_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:8000"
6 |
7 | // 分析開始のパラメータ
8 | export interface StartAnalysisParams {
9 | space_id: string
10 | query: string
11 | tables?: string[]
12 | mode?: string
13 | model?: string
14 | index?: number
15 | }
16 |
17 | // 分析開始のレスポンス
18 | export interface StartAnalysisResponse {
19 | id?: string
20 | error?: string
21 | }
22 |
23 | // スペース作成のレスポンス
24 | export interface CreateSpaceResponse {
25 | id: string
26 | }
27 |
28 | // スペース取得のレスポンス
29 | export interface GetSpaceResponse {
30 | analysis_ids: string[]
31 | }
32 |
33 | // レポート取得のレスポンス
34 | export interface ReportResponse {
35 | done: boolean
36 | progress: string
37 | query: string
38 | error: string
39 | python_code: string
40 | content: ReportContent[]
41 | steps: ActionStep[]
42 | followups?: FollowupContent[]
43 | }
44 |
45 | export interface FollowupContent {
46 | progress: string
47 | query: string
48 | error: string
49 | python_code: string
50 | content: ReportContent[]
51 | steps: ActionStep[]
52 | }
53 |
54 | export interface ActionStep {
55 | type: string
56 | query?: string
57 | python?: string
58 | content?: string
59 | }
60 |
61 | // モデル情報の型定義
62 | export interface ModelInfo {
63 | id: string
64 | name: string
65 | description: string
66 | }
67 |
68 | // モデルリスト取得のレスポンス
69 | export interface ModelListResponse {
70 | models: ModelInfo[]
71 | }
72 |
73 | // レポートコンテンツの型定義
74 | export type ReportContent =
75 | | { type: 'markdown'; content: string }
76 | | { type: 'variable'; data: string }
77 | | { type: 'image'; base64: string }
78 | | { type: 'table'; table: string }
79 |
80 | // モデルリスト取得API (base_url, api_key をクエリパラメータで渡す)
81 | const getModelList = async (params: { base_url?: string; api_key?: string }): Promise => {
82 | const response = await axios.get(`${API_BASE_URL}/get-model-list`, {
83 | params: {
84 | base_url: params.base_url || undefined,
85 | api_key: params.api_key || undefined,
86 | },
87 | })
88 | return response.data
89 | }
90 |
91 | //スペースの作成と取得API
92 | const createSpace = async (): Promise => {
93 | const response = await axios.post(`${API_BASE_URL}/create-space`)
94 | return response.data
95 | }
96 |
97 | const getSpace = async (id: string): Promise => {
98 | const response = await axios.get(`${API_BASE_URL}/get-space/${id}`)
99 | return response.data
100 | }
101 |
102 | // 分析開始API
103 | const startAnalysis = async (params: StartAnalysisParams): Promise => {
104 | const response = await axios.post(`${API_BASE_URL}/start-analysis`, {
105 | space_id: params.space_id,
106 | query: params.query,
107 | tables: params.tables || [],
108 | mode: params.mode || 'standard',
109 | model: params.model || '',
110 | index: params.index,
111 | })
112 | return response.data
113 | }
114 |
115 | // レポート取得API
116 | const getReport = async (id: string): Promise => {
117 | const response = await axios.get(`${API_BASE_URL}/get-report`, {
118 | params: { id }
119 | })
120 | return response.data
121 | }
122 |
123 | // モデルリスト取得フック
124 | export const useModelList = () => {
125 | const { baseUrl, apiKey } = useSettings()
126 | return useQuery({
127 | queryKey: ['modelList', baseUrl, !!apiKey],
128 | queryFn: () => getModelList({ base_url: baseUrl, api_key: apiKey || "" }),
129 | enabled: !!baseUrl, // baseUrl が設定されている場合のみ
130 | staleTime: 50,
131 | })
132 | }
133 |
134 | // モード別モデルリスト取得フック(統合版)
135 | export const useModelListByMode = (isAgentic: boolean) => {
136 | const { baseUrl, apiKey } = useSettings()
137 | return useQuery({
138 | queryKey: ['modelList', isAgentic ? 'agentic' : 'standard', baseUrl, !!apiKey],
139 | queryFn: () => getModelList({ base_url: baseUrl, api_key: apiKey }),
140 | enabled: !!baseUrl,
141 | staleTime: 5 * 60 * 1000,
142 | })
143 | }
144 |
145 | // 分析開始フック
146 | export const useStartAnalysis = () => {
147 | return useMutation({
148 | mutationFn: startAnalysis,
149 | })
150 | }
151 |
152 | // スペース作成フック
153 | export const useCreateSpace = () => {
154 | return useMutation({
155 | mutationFn: createSpace,
156 | })
157 | }
158 |
159 | // スペース取得フック
160 | export const useGetSpace = (id: string) => {
161 | return useQuery({
162 | queryKey: ['space', id],
163 | queryFn: () => getSpace(id),
164 | enabled: !!id, // idが存在する場合のみ実行
165 | staleTime: 5 * 60 * 1000, // 5分間キャッシュ
166 | // refetchInterval: 2000, // 2秒ごとに新しい分析が追加されているかチェック
167 | // refetchIntervalInBackground: true,
168 | })
169 | }
170 |
171 | // レポート取得フック(ポーリング対応)
172 | export const useReport = (id: string, enabled: boolean = true) => {
173 | return useQuery({
174 | queryKey: ['report', id],
175 | queryFn: () => getReport(id),
176 | enabled: enabled && !!id,
177 | refetchInterval: (query) => {
178 | // doneがfalseの場合は1000ミリ秒後に再取得
179 | return query.state.data?.done === false ? 1000 : false
180 | },
181 | refetchIntervalInBackground: true,
182 | })
183 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # quelmap - Open Source Local Data Analysis Assistant.
2 |
3 | 
4 |
5 | # Key Features
6 |
7 | - 📊 Data visualization
8 | - 🚀 Table joins
9 | - 📈 Run statistical tests directly on your dataset
10 | - 📂 Unlimited rows, 30+ tables analyzed simultaneously
11 | - 🐍 Built-in python sandbox
12 | - 🦙 Ollama integration
13 | and more..
14 |
15 | 16GB RAM is enough for practical performance using [Lightning-4b](https://quelmap.com/lightning-4b)
16 |
17 | # Quick Start
18 |
19 | Introduce you how to run **quelmap** on your machine.
20 |
21 | 1. Ensure Docker is installed and running on your system.
22 |
23 | 2. Clone the quelmap repository:
24 | ```bash
25 | git clone https://github.com/quelmap-inc/quelmap.git
26 | ```
27 |
28 | 3. After cloning, navigate to the directory containing the project files.
29 | ```bash
30 | cd quelmap
31 | ```
32 |
33 | 4. Start the application
34 | ```bash
35 | docker compose up --build -d
36 | ```
37 | This command sets up 3 containers(Application, Python Sandbox, PostgresDB). Wait for starting up all containers, Access "http://localhost:3030".
38 | You can use any LLM provider’s model by setting the base_url and api_key from the settings icon in the top right (by default, Ollama is used).
39 | - ollama (default)
40 | - llama.cpp
41 | - LM Studio
42 | - vllm
43 | - OpenAI
44 | - Anthropic
45 | - Groq
46 | - (And more)
47 |
48 | **If you don't know what is base_url and api_key, please google "(Provider Name such as llama.cpp) openai compatible"*
49 |
50 | If you set an LLM provider such as OpenAI or Groq, your dataset schema will be sent. If you want to run everything completely locally, please proceed to the LLM setup steps below.
51 |
52 |
53 | ### LLM Setup
54 | 5. Install ollama (Skip if you already installed)
55 | https://ollama.com/
56 |
57 | 6. Download model (Lightning-4b)
58 | 
59 |
60 | Lightning-4b is lightweight model specially trained to use in quelmap. This model offers greater stability than models with 50 times more parameters in common analytical tasks.
61 | [Detail of Lightning 4b training and performance](https://quelmap.com/lightning-4b)
62 |
63 |
64 | - Laptop (ex. mac book air 16GB) - 4bit Quantization + 10240 Context Window
65 | ```
66 | ollama pull hf.co/quelmap/Lightning-4b-GGUF-short-ctx:Q4_K_M
67 | ```
68 | - Gaming Laptop - 4bit Quantization + 40960 Context Window
69 | ```
70 | ollama pull hf.co/quelmap/Lightning-4b-GGUF:Q4_K_M
71 | ```
72 | - Powerful PC with GPU - No Quantization + 40960 Context Window
73 | ```
74 | ollama pull hf.co/quelmap/Lightning-4b-GGUF:F16
75 | ```
76 |
77 |
78 | (You can use another model like qwen3-next if you have enough GPU.)
79 |
80 | If you feel response is too slow, add "/no_think" or "Do not think." in your query and It will generate python code immediately.
81 |
82 | After downloading the model, hit reload button on your browser and you can choose the model in UI. If no model appeared, check ollama port is 11434. If not, modify port of base_url from the settings icon in the top right.
83 |
84 |
85 |
86 | ### Sample Dataset & Query
87 | For those who don’t have data on hand to analyze, we’ve prepared a sample dataset to try out **quelmap**. Please download the CSV file from this link below and upload it using the **“new tables”** button.
88 | [employee-attrition-dataset](https://quelmap.com/sample_dataset)
89 |
90 | Sample queries:
91 | 1. Create a **scatter plot** of **Age** versus **Total Working Years**.
92 | 2. Use a **Chi-square test** to examine whether there is a difference in **business travel frequency** between **single and married employees**.
93 | 3. Visualize the relationship between **Performance Rating** and **Number of Training Sessions attended last year**.
94 | 4. Use a **boxplot** to show the relationship between **Attrition** and **Monthly Income**, and check whether employees who left tend to have lower monthly income.
95 | 5. Analyze the correlation between **Relationship Satisfaction with Manager** and **Years with Current Manager**.
96 | 6. Perform a **t-test** to test whether there is a difference in **Job Satisfaction** between employees who have worked at **one or more previous companies** and those who have worked at **none**.
97 |
98 | ### Uploadable data type
99 | Following type of data can be uploaded from web UI.
100 | - CSV file
101 | - EXCEL file
102 | - SQlite db file
103 |
104 | Once you upload files, the data will be converted into data table and stored in container volume.
105 |
106 | ### Connect to Database with connection string (Preview)
107 | Normally, uploaded data is stored in the volume of the Postgres container, but you can also execute SQL directly against an external database. A connection string with read permissions is required. (Since unexpected changes may occur, it is recommended to use a user with read-only permissions.)
108 |
109 | 1. Stop containers if running
110 | ```bash
111 | docker compose down -v
112 | ```
113 | 2. Set connection string in .env file
114 | ```bash
115 | echo "USER_DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5432/mydatabase" >> .env
116 | ```
117 | 3. Start container
118 | ```bash
119 | docker compose up --build -d
120 | ```
121 |
122 | ## Contribution
123 | If you find bugs or have ideas, please share them in via GitHub Issues. For more information on contributing to quelmap you can read the [CONTRIBUTING.md](CONTRIBUTING.md) file to learn more about quelmap and how you can contribute to it.
124 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Command as CommandPrimitive } from 'cmdk'
3 | import { SearchIcon } from 'lucide-react'
4 | import { cn } from '@/lib/utils'
5 | import {
6 | Dialog,
7 | DialogContent,
8 | DialogDescription,
9 | DialogHeader,
10 | DialogTitle,
11 | } from '@/components/ui/dialog'
12 |
13 | function Command({
14 | className,
15 | ...props
16 | }: React.ComponentProps) {
17 | return (
18 |
26 | )
27 | }
28 |
29 | function CommandDialog({
30 | title = 'Command Palette',
31 | description = 'Search for a command to run...',
32 | children,
33 | ...props
34 | }: React.ComponentProps & {
35 | title?: string
36 | description?: string
37 | }) {
38 | return (
39 |
50 | )
51 | }
52 |
53 | function CommandInput({
54 | className,
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
62 |
63 |
71 |
72 | )
73 | }
74 |
75 | function CommandList({
76 | className,
77 | ...props
78 | }: React.ComponentProps) {
79 | return (
80 |
88 | )
89 | }
90 |
91 | function CommandEmpty({
92 | ...props
93 | }: React.ComponentProps) {
94 | return (
95 |
100 | )
101 | }
102 |
103 | function CommandGroup({
104 | className,
105 | ...props
106 | }: React.ComponentProps) {
107 | return (
108 |
116 | )
117 | }
118 |
119 | function CommandSeparator({
120 | className,
121 | ...props
122 | }: React.ComponentProps) {
123 | return (
124 |
129 | )
130 | }
131 |
132 | function CommandItem({
133 | className,
134 | ...props
135 | }: React.ComponentProps) {
136 | return (
137 |
145 | )
146 | }
147 |
148 | function CommandShortcut({
149 | className,
150 | ...props
151 | }: React.ComponentProps<'span'>) {
152 | return (
153 |
161 | )
162 | }
163 |
164 | export {
165 | Command,
166 | CommandDialog,
167 | CommandInput,
168 | CommandList,
169 | CommandEmpty,
170 | CommandGroup,
171 | CommandItem,
172 | CommandShortcut,
173 | CommandSeparator,
174 | }
175 |
--------------------------------------------------------------------------------
|