ltcai 0.1.30 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +233 -184
- package/auto_setup.py +279 -55
- package/docs/CHANGELOG.md +69 -0
- package/knowledge_graph.py +1338 -3
- package/knowledge_graph_api.py +112 -0
- package/latticeai/__init__.py +1 -0
- package/latticeai/__pycache__/__init__.cpython-314.pyc +0 -0
- package/latticeai/api/__init__.py +1 -0
- package/latticeai/api/__pycache__/admin.cpython-314.pyc +0 -0
- package/latticeai/api/__pycache__/auth.cpython-314.pyc +0 -0
- package/latticeai/api/admin.py +187 -0
- package/latticeai/api/auth.py +233 -0
- package/latticeai/core/__init__.py +1 -0
- package/latticeai/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/latticeai/core/__pycache__/audit.cpython-314.pyc +0 -0
- package/latticeai/core/__pycache__/security.cpython-314.pyc +0 -0
- package/latticeai/core/__pycache__/sessions.cpython-314.pyc +0 -0
- package/latticeai/core/audit.py +245 -0
- package/latticeai/core/security.py +131 -0
- package/latticeai/core/sessions.py +72 -0
- package/llm_router.py +13 -7
- package/local_knowledge_api.py +319 -0
- package/package.json +5 -2
- package/requirements.txt +2 -1
- package/server.py +290 -901
- package/static/graph.html +7 -2
- package/static/lattice-reference.css +220 -0
- package/static/scripts/graph.js +305 -4
package/static/graph.html
CHANGED
|
@@ -73,12 +73,17 @@
|
|
|
73
73
|
</div>
|
|
74
74
|
|
|
75
75
|
<div class="section">
|
|
76
|
-
<div class="section-label"
|
|
76
|
+
<div class="section-label" id="local-source-label">지식 소스</div>
|
|
77
|
+
<div id="local-source-panel" class="local-source-panel"></div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="section">
|
|
81
|
+
<div class="section-label" id="edge-label">Relationship legend</div>
|
|
77
82
|
<div id="edge-legend" class="legend-grid"></div>
|
|
78
83
|
</div>
|
|
79
84
|
|
|
80
85
|
<div class="section">
|
|
81
|
-
<div class="section-label">Node types</div>
|
|
86
|
+
<div class="section-label" id="type-label">Node types</div>
|
|
82
87
|
<div id="type-filters" class="filter-grid"></div>
|
|
83
88
|
</div>
|
|
84
89
|
|
|
@@ -2947,6 +2947,226 @@ body.lattice-ref-graph {
|
|
|
2947
2947
|
gap: 7px;
|
|
2948
2948
|
}
|
|
2949
2949
|
|
|
2950
|
+
.local-source-panel {
|
|
2951
|
+
display: flex;
|
|
2952
|
+
flex-direction: column;
|
|
2953
|
+
gap: 10px;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
.local-source-notice {
|
|
2957
|
+
border: 1px solid rgba(13,148,136,0.20);
|
|
2958
|
+
border-radius: 8px;
|
|
2959
|
+
background: rgba(13,148,136,0.07);
|
|
2960
|
+
color: #21514b;
|
|
2961
|
+
padding: 9px 10px;
|
|
2962
|
+
font-size: 12px;
|
|
2963
|
+
line-height: 1.5;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
.local-source-input {
|
|
2967
|
+
display: flex;
|
|
2968
|
+
gap: 8px;
|
|
2969
|
+
align-items: center;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
.local-source-input input {
|
|
2973
|
+
min-width: 0;
|
|
2974
|
+
flex: 1;
|
|
2975
|
+
height: 38px;
|
|
2976
|
+
border-radius: 8px;
|
|
2977
|
+
border: 1px solid rgba(111,66,232,0.16);
|
|
2978
|
+
background: #fff;
|
|
2979
|
+
color: var(--text);
|
|
2980
|
+
padding: 0 10px;
|
|
2981
|
+
font-size: 12px;
|
|
2982
|
+
outline: none;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
.local-source-input input:focus {
|
|
2986
|
+
border-color: rgba(111,66,232,0.42);
|
|
2987
|
+
box-shadow: 0 0 0 3px rgba(111,66,232,0.08);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
.local-root-list,
|
|
2991
|
+
.local-tree-list,
|
|
2992
|
+
.local-source-list {
|
|
2993
|
+
display: flex;
|
|
2994
|
+
flex-direction: column;
|
|
2995
|
+
gap: 6px;
|
|
2996
|
+
max-height: 150px;
|
|
2997
|
+
overflow-y: auto;
|
|
2998
|
+
padding-right: 2px;
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
.local-root-btn,
|
|
3002
|
+
.local-tree-row,
|
|
3003
|
+
.local-source-row {
|
|
3004
|
+
width: 100%;
|
|
3005
|
+
border: 1px solid rgba(111,66,232,0.13);
|
|
3006
|
+
border-radius: 8px;
|
|
3007
|
+
background: rgba(255,255,255,0.80);
|
|
3008
|
+
color: var(--text);
|
|
3009
|
+
padding: 8px 9px;
|
|
3010
|
+
display: grid;
|
|
3011
|
+
grid-template-columns: 18px minmax(0, 1fr) auto;
|
|
3012
|
+
gap: 8px;
|
|
3013
|
+
align-items: center;
|
|
3014
|
+
text-align: left;
|
|
3015
|
+
font-size: 12px;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
.local-root-btn {
|
|
3019
|
+
cursor: pointer;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
.local-root-btn:hover,
|
|
3023
|
+
.local-root-btn.active {
|
|
3024
|
+
border-color: rgba(111,66,232,0.34);
|
|
3025
|
+
background: rgba(111,66,232,0.07);
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
.local-source-main,
|
|
3029
|
+
.local-tree-main {
|
|
3030
|
+
min-width: 0;
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
.local-source-main strong,
|
|
3034
|
+
.local-tree-main strong {
|
|
3035
|
+
display: block;
|
|
3036
|
+
font-size: 12px;
|
|
3037
|
+
line-height: 1.25;
|
|
3038
|
+
overflow: hidden;
|
|
3039
|
+
text-overflow: ellipsis;
|
|
3040
|
+
white-space: nowrap;
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
.local-source-main span,
|
|
3044
|
+
.local-tree-main span {
|
|
3045
|
+
display: block;
|
|
3046
|
+
color: var(--faint);
|
|
3047
|
+
font-size: 11px;
|
|
3048
|
+
line-height: 1.35;
|
|
3049
|
+
overflow: hidden;
|
|
3050
|
+
text-overflow: ellipsis;
|
|
3051
|
+
white-space: nowrap;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
.local-source-actions {
|
|
3055
|
+
display: grid;
|
|
3056
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3057
|
+
gap: 7px;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
.local-source-btn {
|
|
3061
|
+
min-width: 0;
|
|
3062
|
+
height: 36px;
|
|
3063
|
+
border: 1px solid rgba(111,66,232,0.18);
|
|
3064
|
+
border-radius: 8px;
|
|
3065
|
+
background: #fff;
|
|
3066
|
+
color: var(--text);
|
|
3067
|
+
cursor: pointer;
|
|
3068
|
+
display: inline-flex;
|
|
3069
|
+
align-items: center;
|
|
3070
|
+
justify-content: center;
|
|
3071
|
+
gap: 6px;
|
|
3072
|
+
font-size: 12px;
|
|
3073
|
+
font-weight: 650;
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
.local-source-btn:hover {
|
|
3077
|
+
border-color: rgba(111,66,232,0.42);
|
|
3078
|
+
color: var(--accent);
|
|
3079
|
+
background: rgba(111,66,232,0.07);
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
.local-source-btn.primary {
|
|
3083
|
+
grid-column: 1 / -1;
|
|
3084
|
+
background: #14162c;
|
|
3085
|
+
color: #fff;
|
|
3086
|
+
border-color: #14162c;
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
.local-source-btn.primary:hover {
|
|
3090
|
+
color: #fff;
|
|
3091
|
+
background: #24284a;
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
.local-source-btn:disabled {
|
|
3095
|
+
cursor: not-allowed;
|
|
3096
|
+
opacity: 0.55;
|
|
3097
|
+
transform: none;
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
.local-option-row {
|
|
3101
|
+
display: flex;
|
|
3102
|
+
flex-direction: column;
|
|
3103
|
+
gap: 6px;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
.local-option-row label {
|
|
3107
|
+
display: inline-flex;
|
|
3108
|
+
align-items: center;
|
|
3109
|
+
gap: 6px;
|
|
3110
|
+
color: var(--muted);
|
|
3111
|
+
font-size: 12px;
|
|
3112
|
+
line-height: 1.3;
|
|
3113
|
+
white-space: nowrap;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
.local-option-row input {
|
|
3117
|
+
margin: 0;
|
|
3118
|
+
accent-color: var(--accent);
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
.local-audit-grid {
|
|
3122
|
+
display: grid;
|
|
3123
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
3124
|
+
gap: 6px;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
.local-audit-stat {
|
|
3128
|
+
border: 1px solid rgba(111,66,232,0.13);
|
|
3129
|
+
border-radius: 8px;
|
|
3130
|
+
background: rgba(255,255,255,0.78);
|
|
3131
|
+
padding: 8px 7px;
|
|
3132
|
+
min-width: 0;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
.local-audit-stat strong {
|
|
3136
|
+
display: block;
|
|
3137
|
+
font-size: 15px;
|
|
3138
|
+
line-height: 1.05;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
.local-audit-stat span {
|
|
3142
|
+
display: block;
|
|
3143
|
+
margin-top: 4px;
|
|
3144
|
+
color: var(--faint);
|
|
3145
|
+
font-size: 10px;
|
|
3146
|
+
line-height: 1.2;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
.local-status-line {
|
|
3150
|
+
color: var(--muted);
|
|
3151
|
+
font-size: 12px;
|
|
3152
|
+
line-height: 1.45;
|
|
3153
|
+
word-break: break-word;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
.local-status-line.error {
|
|
3157
|
+
color: #a53131;
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
.local-permission {
|
|
3161
|
+
border: 1px solid rgba(245,158,11,0.28);
|
|
3162
|
+
background: rgba(245,158,11,0.09);
|
|
3163
|
+
border-radius: 8px;
|
|
3164
|
+
padding: 9px;
|
|
3165
|
+
display: flex;
|
|
3166
|
+
flex-direction: column;
|
|
3167
|
+
gap: 8px;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
2950
3170
|
.legend-item,
|
|
2951
3171
|
.filter-item {
|
|
2952
3172
|
display: flex;
|
package/static/scripts/graph.js
CHANGED
|
@@ -14,6 +14,11 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
14
14
|
sidebar_eyebrow: '지식 그래프', sidebar_title: '지식 토폴로지',
|
|
15
15
|
sidebar_sub: '주제의 크기는 중요도 기반으로, 선의 굵기와 색은 관계 종류와 강도를 반영합니다.',
|
|
16
16
|
nodes: '노드', edges: '연결', relationship_legend: '관계 범례', node_types: '노드 유형',
|
|
17
|
+
local_sources: '지식 소스', local_notice: 'Lattice AI는 사용자가 선택한 폴더만 AI 지식으로 변환합니다.',
|
|
18
|
+
local_path_ph: '폴더 경로 입력...', local_roots: '드라이브 선택', local_tree: '폴더 구조 확인',
|
|
19
|
+
local_audit: '안전 검사', local_index: '지식 그래프 만들기', local_ocr: '이미지 글자 인식',
|
|
20
|
+
local_watch: '자동 감지 켜기', local_permission: '권한 승인', local_sources_empty: '아직 추가된 지식 소스가 없습니다.',
|
|
21
|
+
local_indexed: '지식 그래프 생성 완료', local_watch_unavailable: '자동 감지는 watchdog 설치 후 작동합니다.',
|
|
17
22
|
detail_empty: '노드를 클릭하면 요약, 중요도, 연결 강도, 메타데이터를 볼 수 있습니다. 검색 패널에서는 서버 검색 결과를 기준으로 더 정확하게 이동할 수 있습니다.',
|
|
18
23
|
detail_empty_short: '노드를 클릭하면 요약, 중요도, 메타데이터를 볼 수 있습니다.',
|
|
19
24
|
refresh: '새로고침', error: '오류', graph_load_fail: '그래프를 불러오지 못했습니다.', graph_refresh_fail: '그래프를 새로고침하지 못했습니다.',
|
|
@@ -31,6 +36,11 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
31
36
|
sidebar_eyebrow: 'Knowledge Graph', sidebar_title: 'Knowledge topology',
|
|
32
37
|
sidebar_sub: 'Topic size follows importance; line width and color reflect relationship type and strength.',
|
|
33
38
|
nodes: 'Nodes', edges: 'Edges', relationship_legend: 'Relationship legend', node_types: 'Node types',
|
|
39
|
+
local_sources: 'Knowledge sources', local_notice: 'Lattice AI only turns folders you choose into AI knowledge.',
|
|
40
|
+
local_path_ph: 'Enter a folder path...', local_roots: 'Drive picker', local_tree: 'Check folders',
|
|
41
|
+
local_audit: 'Safety check', local_index: 'Build graph', local_ocr: 'Image text recognition',
|
|
42
|
+
local_watch: 'Auto watch', local_permission: 'Approve access', local_sources_empty: 'No knowledge sources yet.',
|
|
43
|
+
local_indexed: 'Knowledge graph built', local_watch_unavailable: 'Auto watch works after watchdog is installed.',
|
|
34
44
|
detail_empty: 'Click a node to see its summary, importance, connection strength, and metadata. Search results can jump to more precise nodes.',
|
|
35
45
|
detail_empty_short: 'Click a node to see its summary, importance, and metadata.',
|
|
36
46
|
refresh: 'Refresh', error: 'Error', graph_load_fail: 'Could not load the graph.', graph_refresh_fail: 'Could not refresh the graph.',
|
|
@@ -60,8 +70,9 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
60
70
|
document.querySelector('.sidebar-sub').textContent = t('sidebar_sub');
|
|
61
71
|
document.querySelectorAll('.stat span')[0].textContent = t('nodes');
|
|
62
72
|
document.querySelectorAll('.stat span')[1].textContent = t('edges');
|
|
63
|
-
document.
|
|
64
|
-
document.
|
|
73
|
+
document.getElementById('local-source-label').textContent = t('local_sources');
|
|
74
|
+
document.getElementById('edge-label').textContent = t('relationship_legend');
|
|
75
|
+
document.getElementById('type-label').textContent = t('node_types');
|
|
65
76
|
document.getElementById('refresh-btn').textContent = `↺ ${t('refresh')}`;
|
|
66
77
|
const langBtn = document.getElementById('graph-lang-btn');
|
|
67
78
|
if (langBtn) langBtn.textContent = `Language: ${currentLang === 'ko' ? '한국어' : 'English'}`;
|
|
@@ -88,22 +99,32 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
88
99
|
renderSearchResults();
|
|
89
100
|
renderTypeFilters(buildTypeCounts());
|
|
90
101
|
renderEdgeLegend(buildEdgeCounts());
|
|
102
|
+
renderLocalSources();
|
|
91
103
|
showDetail(selected);
|
|
92
104
|
}
|
|
93
105
|
window.toggleLangMenu = toggleLangMenu;
|
|
94
106
|
window.setLang = setLang;
|
|
95
107
|
|
|
96
108
|
const TYPE_CONFIG = {
|
|
109
|
+
Computer: { color: '#14b8a6', label: 'Computer' },
|
|
110
|
+
Drive: { color: '#38bdf8', label: 'Drive' },
|
|
111
|
+
Folder: { color: '#f0a500', label: 'Folder' },
|
|
97
112
|
Conversation: { color: '#9b8af0', label: 'Conversation' },
|
|
98
113
|
Message: { color: '#b8a9f5', label: 'Message' },
|
|
99
114
|
AIResponse: { color: '#6f42e8', label: 'AI Response' },
|
|
100
115
|
File: { color: '#5b9cf6', label: 'File' },
|
|
116
|
+
Document: { color: '#5b9cf6', label: 'Document' },
|
|
117
|
+
CodeFile: { color: '#22c55e', label: 'Code File' },
|
|
118
|
+
Spreadsheet: { color: '#059669', label: 'Spreadsheet' },
|
|
119
|
+
SlideDeck: { color: '#818cf8', label: 'Slide Deck' },
|
|
101
120
|
Topic: { color: '#7c3aed', label: 'Topic' },
|
|
121
|
+
Concept: { color: '#7c3aed', label: 'Concept' },
|
|
102
122
|
Person: { color: '#0d9488', label: 'Person' },
|
|
103
123
|
Page: { color: '#a78bfa', label: 'Page' },
|
|
104
124
|
Slide: { color: '#818cf8', label: 'Slide' },
|
|
105
125
|
Sheet: { color: '#059669', label: 'Sheet' },
|
|
106
126
|
Image: { color: '#d97706', label: 'Image' },
|
|
127
|
+
ImageText: { color: '#f97316', label: 'Image Text' },
|
|
107
128
|
Decision: { color: '#f59e0b', label: 'Decision' },
|
|
108
129
|
Task: { color: '#ec4899', label: 'Task' },
|
|
109
130
|
ClearEvent: { color: '#6366f1', label: 'Clear Event' },
|
|
@@ -126,6 +147,9 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
126
147
|
has_sheet: { color: '#20b8aa', label: 'Sheet', width: 1.3 },
|
|
127
148
|
contains_image: { color: '#f1c86d', label: 'Image', width: 1.35 },
|
|
128
149
|
has_chunk: { color: '#4e566f', label: 'Chunk', width: 0.9, dash: [2, 5] },
|
|
150
|
+
'포함함': { color: '#7186c8', label: 'Contains', width: 1.35 },
|
|
151
|
+
'언급함': { color: '#aebcff', label: 'Mentions', width: 1.45 },
|
|
152
|
+
'관련됨': { color: '#7f8f9d', label: 'Related', width: 1.3 },
|
|
129
153
|
};
|
|
130
154
|
|
|
131
155
|
const canvas = document.getElementById('graph');
|
|
@@ -135,6 +159,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
135
159
|
const searchInput = document.getElementById('search');
|
|
136
160
|
const searchResultsEl = document.getElementById('search-results');
|
|
137
161
|
const searchCountEl = document.getElementById('search-count');
|
|
162
|
+
const localSourcePanel = document.getElementById('local-source-panel');
|
|
138
163
|
|
|
139
164
|
let rawGraph = { nodes: [], edges: [] };
|
|
140
165
|
let graph = { nodes: [], edges: [] };
|
|
@@ -151,6 +176,20 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
151
176
|
let searchResultIds = new Set();
|
|
152
177
|
let searchAbortController = null;
|
|
153
178
|
let searchDebounceId = null;
|
|
179
|
+
let localState = {
|
|
180
|
+
roots: [],
|
|
181
|
+
sources: [],
|
|
182
|
+
watch: null,
|
|
183
|
+
selectedPath: '',
|
|
184
|
+
tree: null,
|
|
185
|
+
audit: null,
|
|
186
|
+
includeOcr: false,
|
|
187
|
+
watchEnabled: false,
|
|
188
|
+
busy: false,
|
|
189
|
+
status: '',
|
|
190
|
+
error: '',
|
|
191
|
+
pendingPermission: null,
|
|
192
|
+
};
|
|
154
193
|
|
|
155
194
|
function apiFetch(path, opts = {}) {
|
|
156
195
|
return fetch(`${API_BASE}${path}`, {
|
|
@@ -173,6 +212,266 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
173
212
|
.replaceAll("'", ''');
|
|
174
213
|
}
|
|
175
214
|
|
|
215
|
+
function formatCount(value) {
|
|
216
|
+
return Number(value || 0).toLocaleString();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function apiJson(path, payload) {
|
|
220
|
+
return apiFetch(path, {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { 'Content-Type': 'application/json' },
|
|
223
|
+
body: JSON.stringify(payload || {}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function loadLocalSources() {
|
|
228
|
+
try {
|
|
229
|
+
const [rootsRes, sourcesRes] = await Promise.all([
|
|
230
|
+
apiFetch('/knowledge-graph/local/roots'),
|
|
231
|
+
apiFetch('/knowledge-graph/local/sources'),
|
|
232
|
+
]);
|
|
233
|
+
if (rootsRes.status === 401 || sourcesRes.status === 401) {
|
|
234
|
+
window.location.href = '/account';
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const rootsData = rootsRes.ok ? await rootsRes.json() : {};
|
|
238
|
+
const sourcesData = sourcesRes.ok ? await sourcesRes.json() : {};
|
|
239
|
+
localState.roots = Array.isArray(rootsData.roots) ? rootsData.roots : [];
|
|
240
|
+
localState.sources = Array.isArray(sourcesData.sources) ? sourcesData.sources : [];
|
|
241
|
+
localState.watch = sourcesData.watch || null;
|
|
242
|
+
if (!localState.selectedPath && localState.roots[0]) {
|
|
243
|
+
localState.selectedPath = localState.roots[0].path;
|
|
244
|
+
}
|
|
245
|
+
renderLocalSources();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
localState.error = error.message;
|
|
248
|
+
renderLocalSources();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderLocalSources() {
|
|
253
|
+
if (!localSourcePanel) return;
|
|
254
|
+
const rootRows = localState.roots.slice(0, 8).map(root => {
|
|
255
|
+
const active = root.path === localState.selectedPath ? 'active' : '';
|
|
256
|
+
return `
|
|
257
|
+
<button class="local-root-btn ${active}" onclick="selectLocalPath(decodeURIComponent('${encodeURIComponent(root.path)}'))" title="${escapeHtml(root.path)}">
|
|
258
|
+
<i class="ti ${root.kind === 'drive' || root.kind === 'volume' ? 'ti-device-desktop' : 'ti-folder'}"></i>
|
|
259
|
+
<span class="local-source-main">
|
|
260
|
+
<strong>${escapeHtml(root.label || root.path)}</strong>
|
|
261
|
+
<span>${escapeHtml(root.path)}</span>
|
|
262
|
+
</span>
|
|
263
|
+
${root.warning ? '<i class="ti ti-alert-triangle"></i>' : ''}
|
|
264
|
+
</button>
|
|
265
|
+
`;
|
|
266
|
+
}).join('');
|
|
267
|
+
|
|
268
|
+
const treeRows = (localState.tree?.items || []).slice(0, 8).map(item => `
|
|
269
|
+
<div class="local-tree-row" title="${escapeHtml(item.path)}">
|
|
270
|
+
<i class="ti ${item.type === 'directory' ? 'ti-folder' : 'ti-file'}"></i>
|
|
271
|
+
<span class="local-tree-main">
|
|
272
|
+
<strong>${escapeHtml(item.name)}</strong>
|
|
273
|
+
<span>${escapeHtml(item.excluded_reason || item.extension || item.type)}</span>
|
|
274
|
+
</span>
|
|
275
|
+
${item.accessible === false ? '<i class="ti ti-lock"></i>' : ''}
|
|
276
|
+
</div>
|
|
277
|
+
`).join('');
|
|
278
|
+
|
|
279
|
+
const summary = localState.audit?.summary || null;
|
|
280
|
+
const auditHtml = summary ? `
|
|
281
|
+
<div class="local-audit-grid">
|
|
282
|
+
<div class="local-audit-stat"><strong>${formatCount(summary.readable_files)}</strong><span>읽을 파일</span></div>
|
|
283
|
+
<div class="local-audit-stat"><strong>${formatCount(summary.sensitive_files)}</strong><span>민감 제외</span></div>
|
|
284
|
+
<div class="local-audit-stat"><strong>${formatCount(summary.unsupported_files)}</strong><span>미지원</span></div>
|
|
285
|
+
<div class="local-audit-stat"><strong>${formatCount(summary.too_large_files)}</strong><span>너무 큼</span></div>
|
|
286
|
+
<div class="local-audit-stat"><strong>${formatCount(summary.image_ocr_candidates)}</strong><span>이미지</span></div>
|
|
287
|
+
<div class="local-audit-stat"><strong>${formatCount(summary.estimated_seconds)}</strong><span>예상 초</span></div>
|
|
288
|
+
</div>
|
|
289
|
+
` : '';
|
|
290
|
+
|
|
291
|
+
const permissionHtml = localState.pendingPermission ? `
|
|
292
|
+
<div class="local-permission">
|
|
293
|
+
<div class="local-status-line">${escapeHtml(localState.pendingPermission.message || '')}</div>
|
|
294
|
+
<button class="local-source-btn primary" onclick="approveLocalPermission()">
|
|
295
|
+
<i class="ti ti-shield-check"></i>${t('local_permission')}
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
` : '';
|
|
299
|
+
|
|
300
|
+
const sourceRows = localState.sources.slice(0, 4).map(source => {
|
|
301
|
+
const status = source.watch_active ? '자동 감지 중' : (source.watch_enabled ? '자동 감지 대기' : '수동 반영');
|
|
302
|
+
return `
|
|
303
|
+
<div class="local-source-row" title="${escapeHtml(source.root_path)}">
|
|
304
|
+
<i class="ti ti-database"></i>
|
|
305
|
+
<span class="local-source-main">
|
|
306
|
+
<strong>${escapeHtml(source.label || source.root_path)}</strong>
|
|
307
|
+
<span>${escapeHtml(status)} · ${escapeHtml(source.root_path)}</span>
|
|
308
|
+
</span>
|
|
309
|
+
<span>${formatCount((source.file_status || {}).indexed)}</span>
|
|
310
|
+
</div>
|
|
311
|
+
`;
|
|
312
|
+
}).join('');
|
|
313
|
+
|
|
314
|
+
const watchWarning = localState.watch && localState.watch.available === false
|
|
315
|
+
? `<div class="local-status-line">${t('local_watch_unavailable')}</div>`
|
|
316
|
+
: '';
|
|
317
|
+
const statusClass = localState.error ? ' error' : '';
|
|
318
|
+
const statusText = localState.error || localState.status || '';
|
|
319
|
+
|
|
320
|
+
localSourcePanel.innerHTML = `
|
|
321
|
+
<div class="local-source-notice">${t('local_notice')}</div>
|
|
322
|
+
<div class="local-source-input">
|
|
323
|
+
<input id="local-path-input" value="${escapeHtml(localState.selectedPath)}" placeholder="${t('local_path_ph')}" oninput="updateLocalPath(this.value)">
|
|
324
|
+
</div>
|
|
325
|
+
${rootRows ? `<div class="local-root-list">${rootRows}</div>` : ''}
|
|
326
|
+
<div class="local-option-row">
|
|
327
|
+
<label><input type="checkbox" ${localState.includeOcr ? 'checked' : ''} onchange="setLocalOption('includeOcr', this.checked)"> ${t('local_ocr')}</label>
|
|
328
|
+
<label><input type="checkbox" ${localState.watchEnabled ? 'checked' : ''} onchange="setLocalOption('watchEnabled', this.checked)"> ${t('local_watch')}</label>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="local-source-actions">
|
|
331
|
+
<button class="local-source-btn" ${localState.busy ? 'disabled' : ''} onclick="runLocalTree()" title="${t('local_tree')}"><i class="ti ti-folders"></i>${t('local_tree')}</button>
|
|
332
|
+
<button class="local-source-btn" ${localState.busy ? 'disabled' : ''} onclick="runLocalAudit()" title="${t('local_audit')}"><i class="ti ti-shield-search"></i>${t('local_audit')}</button>
|
|
333
|
+
<button class="local-source-btn primary" ${localState.busy ? 'disabled' : ''} onclick="runLocalIndex()" title="${t('local_index')}"><i class="ti ti-chart-dots-3"></i>${t('local_index')}</button>
|
|
334
|
+
</div>
|
|
335
|
+
${permissionHtml}
|
|
336
|
+
${statusText ? `<div class="local-status-line${statusClass}">${escapeHtml(statusText)}</div>` : ''}
|
|
337
|
+
${watchWarning}
|
|
338
|
+
${auditHtml}
|
|
339
|
+
${treeRows ? `<div class="local-tree-list">${treeRows}</div>` : ''}
|
|
340
|
+
<div class="local-source-list">
|
|
341
|
+
${sourceRows || `<div class="local-status-line">${t('local_sources_empty')}</div>`}
|
|
342
|
+
</div>
|
|
343
|
+
`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function selectLocalPath(path) {
|
|
347
|
+
localState.selectedPath = path;
|
|
348
|
+
localState.tree = null;
|
|
349
|
+
localState.audit = null;
|
|
350
|
+
localState.error = '';
|
|
351
|
+
localState.status = '';
|
|
352
|
+
renderLocalSources();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function updateLocalPath(path) {
|
|
356
|
+
localState.selectedPath = path;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function setLocalOption(key, value) {
|
|
360
|
+
localState[key] = Boolean(value);
|
|
361
|
+
renderLocalSources();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function runLocalRequest(endpoint, payload, onSuccess) {
|
|
365
|
+
if (!localState.selectedPath) return;
|
|
366
|
+
localState.busy = true;
|
|
367
|
+
localState.error = '';
|
|
368
|
+
localState.status = '';
|
|
369
|
+
localState.pendingPermission = null;
|
|
370
|
+
renderLocalSources();
|
|
371
|
+
try {
|
|
372
|
+
const res = await apiJson(endpoint, payload);
|
|
373
|
+
if (res.status === 401) {
|
|
374
|
+
window.location.href = '/account';
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const data = await res.json();
|
|
378
|
+
if (data.permission_required) {
|
|
379
|
+
localState.pendingPermission = { endpoint, payload, ...data };
|
|
380
|
+
localState.busy = false;
|
|
381
|
+
renderLocalSources();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!res.ok) throw new Error(data.detail || `Request failed (${res.status})`);
|
|
385
|
+
await onSuccess(data);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
localState.error = error.message;
|
|
388
|
+
} finally {
|
|
389
|
+
localState.busy = false;
|
|
390
|
+
renderLocalSources();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function approveLocalPermission() {
|
|
395
|
+
const pending = localState.pendingPermission;
|
|
396
|
+
if (!pending) return;
|
|
397
|
+
localState.busy = true;
|
|
398
|
+
renderLocalSources();
|
|
399
|
+
try {
|
|
400
|
+
const approveRes = await apiFetch(`/permissions/approve/${encodeURIComponent(pending.approval_token)}`, { method: 'POST' });
|
|
401
|
+
const approveData = await approveRes.json().catch(() => ({}));
|
|
402
|
+
if (!approveRes.ok) throw new Error(approveData.detail || `Approval failed (${approveRes.status})`);
|
|
403
|
+
const payload = { ...pending.payload, approved: true, approval_token: pending.approval_token };
|
|
404
|
+
const res = await apiJson(pending.endpoint, payload);
|
|
405
|
+
const data = await res.json();
|
|
406
|
+
if (!res.ok) throw new Error(data.detail || `Request failed (${res.status})`);
|
|
407
|
+
localState.pendingPermission = null;
|
|
408
|
+
if (pending.endpoint.endsWith('/tree')) {
|
|
409
|
+
localState.tree = data;
|
|
410
|
+
localState.status = data.privacy_notice || '';
|
|
411
|
+
} else if (pending.endpoint.endsWith('/audit')) {
|
|
412
|
+
localState.audit = data;
|
|
413
|
+
localState.status = data.privacy_notice || '';
|
|
414
|
+
} else if (pending.endpoint.endsWith('/index')) {
|
|
415
|
+
localState.status = `${t('local_indexed')} · ${formatCount((data.counts || {}).indexed)} files`;
|
|
416
|
+
await Promise.all([loadGraph(), loadLocalSources()]);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
localState.error = error.message;
|
|
421
|
+
} finally {
|
|
422
|
+
localState.busy = false;
|
|
423
|
+
renderLocalSources();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function runLocalTree() {
|
|
428
|
+
runLocalRequest('/knowledge-graph/local/tree', {
|
|
429
|
+
path: localState.selectedPath,
|
|
430
|
+
max_items: 120,
|
|
431
|
+
}, data => {
|
|
432
|
+
localState.tree = data;
|
|
433
|
+
localState.status = data.privacy_notice || '';
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function runLocalAudit() {
|
|
438
|
+
runLocalRequest('/knowledge-graph/local/audit', {
|
|
439
|
+
path: localState.selectedPath,
|
|
440
|
+
include_ocr: localState.includeOcr,
|
|
441
|
+
max_files: 50000,
|
|
442
|
+
}, data => {
|
|
443
|
+
localState.audit = data;
|
|
444
|
+
localState.status = data.privacy_notice || '';
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function runLocalIndex() {
|
|
449
|
+
runLocalRequest('/knowledge-graph/local/index', {
|
|
450
|
+
path: localState.selectedPath,
|
|
451
|
+
include_ocr: localState.includeOcr,
|
|
452
|
+
watch_enabled: localState.watchEnabled,
|
|
453
|
+
max_files: 5000,
|
|
454
|
+
consent: {
|
|
455
|
+
ui: 'graph',
|
|
456
|
+
knowledge_source: true,
|
|
457
|
+
image_ocr: localState.includeOcr,
|
|
458
|
+
watch_enabled: localState.watchEnabled,
|
|
459
|
+
sensitive_files_default_excluded: true,
|
|
460
|
+
},
|
|
461
|
+
}, async data => {
|
|
462
|
+
localState.status = `${t('local_indexed')} · ${formatCount((data.counts || {}).indexed)} files`;
|
|
463
|
+
await Promise.all([loadGraph(), loadLocalSources()]);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
window.selectLocalPath = selectLocalPath;
|
|
468
|
+
window.updateLocalPath = updateLocalPath;
|
|
469
|
+
window.setLocalOption = setLocalOption;
|
|
470
|
+
window.runLocalTree = runLocalTree;
|
|
471
|
+
window.runLocalAudit = runLocalAudit;
|
|
472
|
+
window.runLocalIndex = runLocalIndex;
|
|
473
|
+
window.approveLocalPermission = approveLocalPermission;
|
|
474
|
+
|
|
176
475
|
function nodeColor(type) {
|
|
177
476
|
return (TYPE_CONFIG[type] || {}).color || '#8fa8bb';
|
|
178
477
|
}
|
|
@@ -712,7 +1011,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
712
1011
|
: '';
|
|
713
1012
|
const metrics = metricCards(node);
|
|
714
1013
|
const updatedAt = formatUpdatedAt(node.updated_at);
|
|
715
|
-
const source = meta.filename || meta.conversation_id || meta.source || '';
|
|
1014
|
+
const source = meta.relative_path || meta.filename || meta.conversation_id || meta.source || '';
|
|
716
1015
|
const metadataStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : '';
|
|
717
1016
|
detail.innerHTML = `
|
|
718
1017
|
<div class="type-badge" style="background:${nodeColor(node.type)}">${escapeHtml(typeLabel(node.type))}</div>
|
|
@@ -756,7 +1055,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
756
1055
|
<div class="search-list">
|
|
757
1056
|
${searchResults.map(match => {
|
|
758
1057
|
const active = selected && selected.id === match.id ? 'active' : '';
|
|
759
|
-
const source = (match.metadata || {}).filename || (match.metadata || {}).conversation_id || '';
|
|
1058
|
+
const source = (match.metadata || {}).relative_path || (match.metadata || {}).filename || (match.metadata || {}).conversation_id || '';
|
|
760
1059
|
return `
|
|
761
1060
|
<button class="search-item ${active}" data-node-id="${escapeHtml(match.id)}">
|
|
762
1061
|
<div class="search-item-top">
|
|
@@ -1050,6 +1349,8 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
1050
1349
|
resize();
|
|
1051
1350
|
applyI18n();
|
|
1052
1351
|
renderSearchResults();
|
|
1352
|
+
renderLocalSources();
|
|
1353
|
+
loadLocalSources();
|
|
1053
1354
|
loadGraph().catch(error => {
|
|
1054
1355
|
detail.innerHTML = `
|
|
1055
1356
|
<div class="type-badge" style="background:${nodeColor('ClearEvent')}">${t('error')}</div>
|