ltcai 0.1.9 → 0.1.16
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 +174 -305
- package/docs/CHANGELOG.md +307 -0
- package/docs/architecture.md +121 -0
- package/docs/mcp-tools.md +116 -0
- package/docs/privacy.md +74 -0
- package/docs/public-deploy.md +137 -0
- package/docs/security-model.md +121 -0
- package/knowledge_graph.py +123 -15
- package/llm_router.py +100 -28
- package/ltcai_cli.py +138 -5
- package/package.json +14 -2
- package/server.py +1756 -329
- package/skills/SKILL_TEMPLATE.md +61 -29
- package/skills/code_review/SKILL.md +28 -0
- package/skills/code_review/examples.md +59 -0
- package/skills/code_review/risk.json +9 -0
- package/skills/code_review/schema.json +65 -0
- package/skills/data_analysis/SKILL.md +28 -0
- package/skills/data_analysis/examples.md +62 -0
- package/skills/data_analysis/risk.json +9 -0
- package/skills/data_analysis/schema.json +61 -0
- package/skills/file_edit/SKILL.md +33 -0
- package/skills/file_edit/examples.md +45 -0
- package/skills/file_edit/risk.json +9 -0
- package/skills/file_edit/schema.json +60 -0
- package/skills/summarize_document/SKILL.md +68 -0
- package/skills/summarize_document/examples.md +65 -0
- package/skills/summarize_document/risk.json +9 -0
- package/skills/summarize_document/schema.json +71 -0
- package/skills/web_search/SKILL.md +28 -0
- package/skills/web_search/examples.md +61 -0
- package/skills/web_search/risk.json +9 -0
- package/skills/web_search/schema.json +62 -0
- package/static/account.html +53 -51
- package/static/admin.html +50 -46
- package/static/chat.html +124 -96
- package/static/graph.html +1231 -337
- package/static/manifest.json +2 -2
- package/tests/integration/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tests/integration/__pycache__/test_api.cpython-314-pytest-9.0.3.pyc +0 -0
- package/tests/unit/__pycache__/test_tools.cpython-314-pytest-9.0.3.pyc +0 -0
- package/tests/unit/test_tools.py +194 -1
- package/tools.py +264 -4
package/static/chat.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- PWA -->
|
|
10
10
|
<link rel="manifest" href="/manifest.json">
|
|
11
|
-
<meta name="theme-color" content="#
|
|
11
|
+
<meta name="theme-color" content="#282a36">
|
|
12
12
|
<meta name="mobile-web-app-capable" content="yes">
|
|
13
13
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
14
14
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
@@ -2724,30 +2724,31 @@
|
|
|
2724
2724
|
}
|
|
2725
2725
|
</style>
|
|
2726
2726
|
<style>
|
|
2727
|
-
/*
|
|
2727
|
+
/* Obsidian-inspired workspace refresh. Scoped to the signed-in app. */
|
|
2728
2728
|
.app-layout {
|
|
2729
|
-
--bg: #
|
|
2730
|
-
--surface: #
|
|
2731
|
-
--surface-2: #
|
|
2732
|
-
--surface-3: #
|
|
2733
|
-
--accent: #
|
|
2734
|
-
--accent-2: #
|
|
2735
|
-
--accent-3: #
|
|
2736
|
-
--accent-pink: #
|
|
2737
|
-
--accent-soft: rgba(
|
|
2738
|
-
--text: #
|
|
2739
|
-
--muted: #
|
|
2740
|
-
--faint: #
|
|
2741
|
-
--border: rgba(
|
|
2742
|
-
--border-strong: rgba(
|
|
2743
|
-
--shadow: 0 22px
|
|
2744
|
-
--shadow-sm: 0 8px
|
|
2745
|
-
--glow-green: 0 0
|
|
2729
|
+
--bg: #282a36;
|
|
2730
|
+
--surface: #222431;
|
|
2731
|
+
--surface-2: #2f3142;
|
|
2732
|
+
--surface-3: #3a3d52;
|
|
2733
|
+
--accent: #a77cff;
|
|
2734
|
+
--accent-2: #f1c86d;
|
|
2735
|
+
--accent-3: #7db7ff;
|
|
2736
|
+
--accent-pink: #ff7db3;
|
|
2737
|
+
--accent-soft: rgba(167, 124, 255, 0.13);
|
|
2738
|
+
--text: #f7f7f2;
|
|
2739
|
+
--muted: #c4c8d8;
|
|
2740
|
+
--faint: #8d93ab;
|
|
2741
|
+
--border: rgba(218, 225, 255, 0.13);
|
|
2742
|
+
--border-strong: rgba(218, 225, 255, 0.23);
|
|
2743
|
+
--shadow: 0 22px 64px rgba(5, 7, 12, 0.34);
|
|
2744
|
+
--shadow-sm: 0 8px 24px rgba(5, 7, 12, 0.24);
|
|
2745
|
+
--glow-green: 0 0 30px rgba(167, 124, 255, 0.14);
|
|
2746
2746
|
position: relative;
|
|
2747
2747
|
isolation: isolate;
|
|
2748
2748
|
background:
|
|
2749
|
+
radial-gradient(circle at 48% 42%, rgba(125,183,255,0.09), transparent 34%),
|
|
2749
2750
|
linear-gradient(180deg, rgba(255,255,255,0.018), transparent 34%),
|
|
2750
|
-
linear-gradient(135deg, #
|
|
2751
|
+
linear-gradient(135deg, #292b38 0%, #242632 52%, #2f3040 100%);
|
|
2751
2752
|
}
|
|
2752
2753
|
|
|
2753
2754
|
.app-layout::before {
|
|
@@ -2757,21 +2758,35 @@
|
|
|
2757
2758
|
z-index: -1;
|
|
2758
2759
|
pointer-events: none;
|
|
2759
2760
|
background:
|
|
2760
|
-
|
|
2761
|
-
linear-gradient(
|
|
2762
|
-
|
|
2763
|
-
|
|
2761
|
+
radial-gradient(circle, rgba(247,247,242,0.7) 1px, transparent 1.8px),
|
|
2762
|
+
linear-gradient(rgba(157,177,255,0.13) 1px, transparent 1px),
|
|
2763
|
+
linear-gradient(90deg, rgba(157,177,255,0.09) 1px, transparent 1px);
|
|
2764
|
+
background-size: 76px 76px, 48px 48px, 48px 48px;
|
|
2765
|
+
background-position: 18px 22px, 0 0, 0 0;
|
|
2766
|
+
mask-image: linear-gradient(180deg, rgba(0,0,0,0.45), rgba(0,0,0,0.09));
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
.app-layout::after {
|
|
2770
|
+
content: '';
|
|
2771
|
+
position: absolute;
|
|
2772
|
+
inset: 0;
|
|
2773
|
+
z-index: -1;
|
|
2774
|
+
pointer-events: none;
|
|
2775
|
+
background:
|
|
2776
|
+
linear-gradient(116deg, transparent 0 42%, rgba(125,183,255,0.08) 42.15%, transparent 42.35% 100%),
|
|
2777
|
+
linear-gradient(28deg, transparent 0 58%, rgba(167,124,255,0.08) 58.15%, transparent 58.35% 100%);
|
|
2778
|
+
opacity: 0.72;
|
|
2764
2779
|
}
|
|
2765
2780
|
|
|
2766
2781
|
.app-layout .sidebar {
|
|
2767
|
-
background: rgba(
|
|
2768
|
-
border-right-color: rgba(
|
|
2782
|
+
background: rgba(30, 32, 43, 0.94);
|
|
2783
|
+
border-right-color: rgba(218,225,255,0.13);
|
|
2769
2784
|
box-shadow: inset -1px 0 0 rgba(255,255,255,0.025);
|
|
2770
2785
|
}
|
|
2771
2786
|
|
|
2772
2787
|
@media (max-width: 768px) {
|
|
2773
2788
|
.app-layout .sidebar {
|
|
2774
|
-
background: #
|
|
2789
|
+
background: #222431;
|
|
2775
2790
|
backdrop-filter: none;
|
|
2776
2791
|
-webkit-backdrop-filter: none;
|
|
2777
2792
|
}
|
|
@@ -2785,9 +2800,9 @@
|
|
|
2785
2800
|
|
|
2786
2801
|
.app-layout .logo-box,
|
|
2787
2802
|
.app-layout .user-avatar {
|
|
2788
|
-
background:
|
|
2789
|
-
color: #
|
|
2790
|
-
box-shadow: 0 10px 24px rgba(0,0,0,0.
|
|
2803
|
+
background: radial-gradient(circle at 35% 30%, #ffffff 0 16%, #a77cff 17% 62%, #5b6cff 100%);
|
|
2804
|
+
color: #171926;
|
|
2805
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.18), 0 10px 24px rgba(0,0,0,0.28), 0 0 26px rgba(167,124,255,0.18);
|
|
2791
2806
|
}
|
|
2792
2807
|
|
|
2793
2808
|
.app-layout .brand-title {
|
|
@@ -2799,20 +2814,20 @@
|
|
|
2799
2814
|
|
|
2800
2815
|
.app-layout .history-item:hover,
|
|
2801
2816
|
.app-layout .history-item.active {
|
|
2802
|
-
background: rgba(
|
|
2817
|
+
background: rgba(167,124,255,0.10);
|
|
2803
2818
|
border-left-color: var(--accent);
|
|
2804
2819
|
}
|
|
2805
2820
|
|
|
2806
2821
|
.app-layout .history-item.active {
|
|
2807
|
-
border-color: rgba(
|
|
2822
|
+
border-color: rgba(167,124,255,0.24);
|
|
2808
2823
|
}
|
|
2809
2824
|
|
|
2810
2825
|
.app-layout .new-chat-btn,
|
|
2811
2826
|
.app-layout .status-btn,
|
|
2812
2827
|
.app-layout .setup-wizard-sidebar-btn,
|
|
2813
2828
|
.app-layout .admin-btn {
|
|
2814
|
-
background: rgba(
|
|
2815
|
-
border-color: rgba(
|
|
2829
|
+
background: rgba(247,247,242,0.045);
|
|
2830
|
+
border-color: rgba(218,225,255,0.12);
|
|
2816
2831
|
color: var(--text);
|
|
2817
2832
|
box-shadow: none;
|
|
2818
2833
|
}
|
|
@@ -2821,8 +2836,8 @@
|
|
|
2821
2836
|
.app-layout .status-btn:hover,
|
|
2822
2837
|
.app-layout .setup-wizard-sidebar-btn:hover,
|
|
2823
2838
|
.app-layout .admin-btn:hover {
|
|
2824
|
-
background: rgba(
|
|
2825
|
-
border-color: rgba(
|
|
2839
|
+
background: rgba(167,124,255,0.12);
|
|
2840
|
+
border-color: rgba(167,124,255,0.3);
|
|
2826
2841
|
color: var(--accent);
|
|
2827
2842
|
}
|
|
2828
2843
|
|
|
@@ -2831,15 +2846,15 @@
|
|
|
2831
2846
|
}
|
|
2832
2847
|
|
|
2833
2848
|
.app-layout .chat-header {
|
|
2834
|
-
background: rgba(
|
|
2835
|
-
border-bottom-color: rgba(
|
|
2849
|
+
background: rgba(33, 35, 48, 0.82);
|
|
2850
|
+
border-bottom-color: rgba(218,225,255,0.12);
|
|
2836
2851
|
box-shadow: 0 1px 0 rgba(255,255,255,0.025);
|
|
2837
2852
|
}
|
|
2838
2853
|
|
|
2839
2854
|
.app-layout .model-badge,
|
|
2840
2855
|
.app-layout .status-pill {
|
|
2841
|
-
background: rgba(
|
|
2842
|
-
border-color: rgba(
|
|
2856
|
+
background: rgba(247,247,242,0.045);
|
|
2857
|
+
border-color: rgba(218,225,255,0.13);
|
|
2843
2858
|
color: var(--muted);
|
|
2844
2859
|
}
|
|
2845
2860
|
|
|
@@ -2848,44 +2863,45 @@
|
|
|
2848
2863
|
}
|
|
2849
2864
|
|
|
2850
2865
|
.app-layout .status-pill.vpc-ready {
|
|
2851
|
-
background: rgba(
|
|
2852
|
-
border-color: rgba(
|
|
2866
|
+
background: rgba(32,184,170,0.11);
|
|
2867
|
+
border-color: rgba(32,184,170,0.28);
|
|
2868
|
+
color: #7ee3d7;
|
|
2853
2869
|
}
|
|
2854
2870
|
|
|
2855
2871
|
.app-layout .status-dot {
|
|
2856
|
-
background:
|
|
2857
|
-
box-shadow: 0 0 8px rgba(
|
|
2872
|
+
background: #f7f7f2;
|
|
2873
|
+
box-shadow: 0 0 8px rgba(247,247,242,0.7), 0 0 18px rgba(167,124,255,0.36);
|
|
2858
2874
|
}
|
|
2859
2875
|
|
|
2860
2876
|
.app-layout .ops-card {
|
|
2861
|
-
background: rgba(
|
|
2862
|
-
border-color: rgba(
|
|
2877
|
+
background: rgba(34, 36, 49, 0.82);
|
|
2878
|
+
border-color: rgba(218,225,255,0.14);
|
|
2863
2879
|
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255,255,255,0.035);
|
|
2864
2880
|
}
|
|
2865
2881
|
|
|
2866
2882
|
.app-layout .ops-card.primary {
|
|
2867
|
-
background: linear-gradient(135deg, rgba(
|
|
2868
|
-
border-color: rgba(
|
|
2883
|
+
background: linear-gradient(135deg, rgba(167,124,255,0.16), rgba(34,36,49,0.9) 58%);
|
|
2884
|
+
border-color: rgba(167,124,255,0.28);
|
|
2869
2885
|
}
|
|
2870
2886
|
|
|
2871
2887
|
.app-layout .ops-card.interactive:hover {
|
|
2872
|
-
background: linear-gradient(135deg, rgba(
|
|
2873
|
-
border-color: rgba(
|
|
2874
|
-
box-shadow: var(--shadow), 0 0
|
|
2888
|
+
background: linear-gradient(135deg, rgba(167,124,255,0.18), rgba(34,36,49,0.96) 62%);
|
|
2889
|
+
border-color: rgba(167,124,255,0.38);
|
|
2890
|
+
box-shadow: var(--shadow), 0 0 26px rgba(167,124,255,0.13);
|
|
2875
2891
|
}
|
|
2876
2892
|
|
|
2877
2893
|
.app-layout .ops-card.primary .ops-icon,
|
|
2878
2894
|
.app-layout .empty-icon,
|
|
2879
2895
|
.app-layout .empty-state > div[style] {
|
|
2880
|
-
background: rgba(
|
|
2881
|
-
border-color: rgba(
|
|
2896
|
+
background: rgba(167,124,255,0.12) !important;
|
|
2897
|
+
border-color: rgba(167,124,255,0.24) !important;
|
|
2882
2898
|
color: var(--accent) !important;
|
|
2883
2899
|
box-shadow: none !important;
|
|
2884
2900
|
}
|
|
2885
2901
|
|
|
2886
2902
|
.app-layout .ops-card:not(.primary) .ops-icon {
|
|
2887
|
-
background: rgba(
|
|
2888
|
-
border-color: rgba(
|
|
2903
|
+
background: rgba(247,247,242,0.045);
|
|
2904
|
+
border-color: rgba(218,225,255,0.1);
|
|
2889
2905
|
color: var(--accent-3);
|
|
2890
2906
|
}
|
|
2891
2907
|
|
|
@@ -2896,28 +2912,28 @@
|
|
|
2896
2912
|
}
|
|
2897
2913
|
|
|
2898
2914
|
.app-layout .empty-chip {
|
|
2899
|
-
background: rgba(
|
|
2900
|
-
border-color: rgba(
|
|
2915
|
+
background: rgba(34,36,49,0.82);
|
|
2916
|
+
border-color: rgba(218,225,255,0.12);
|
|
2901
2917
|
color: var(--muted);
|
|
2902
2918
|
box-shadow: none;
|
|
2903
2919
|
}
|
|
2904
2920
|
|
|
2905
2921
|
.app-layout .empty-chip:hover {
|
|
2906
|
-
background: rgba(
|
|
2907
|
-
border-color: rgba(
|
|
2922
|
+
background: rgba(167,124,255,0.10);
|
|
2923
|
+
border-color: rgba(167,124,255,0.28);
|
|
2908
2924
|
box-shadow: 0 10px 28px rgba(0,0,0,0.2);
|
|
2909
2925
|
}
|
|
2910
2926
|
|
|
2911
2927
|
.app-layout .user .bubble {
|
|
2912
|
-
background:
|
|
2913
|
-
border-color: rgba(
|
|
2914
|
-
color: #
|
|
2928
|
+
background: linear-gradient(135deg, rgba(98,89,196,0.96), rgba(45,116,156,0.92));
|
|
2929
|
+
border-color: rgba(167,124,255,0.24);
|
|
2930
|
+
color: #fbfbff;
|
|
2915
2931
|
box-shadow: 0 8px 24px rgba(0,0,0,0.22);
|
|
2916
2932
|
}
|
|
2917
2933
|
|
|
2918
2934
|
.app-layout .ai .bubble {
|
|
2919
|
-
background: rgba(
|
|
2920
|
-
border-color: rgba(
|
|
2935
|
+
background: rgba(34, 36, 49, 0.9);
|
|
2936
|
+
border-color: rgba(218,225,255,0.13);
|
|
2921
2937
|
box-shadow: 0 8px 24px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.035);
|
|
2922
2938
|
}
|
|
2923
2939
|
|
|
@@ -2930,45 +2946,45 @@
|
|
|
2930
2946
|
|
|
2931
2947
|
.app-layout .code-container,
|
|
2932
2948
|
.app-layout pre {
|
|
2933
|
-
background: #
|
|
2934
|
-
border-color: rgba(
|
|
2949
|
+
background: #171925;
|
|
2950
|
+
border-color: rgba(218,225,255,0.10);
|
|
2935
2951
|
}
|
|
2936
2952
|
|
|
2937
2953
|
.app-layout .code-header {
|
|
2938
|
-
background: #
|
|
2939
|
-
border-bottom-color: rgba(
|
|
2954
|
+
background: #1d2030;
|
|
2955
|
+
border-bottom-color: rgba(218,225,255,0.09);
|
|
2940
2956
|
}
|
|
2941
2957
|
|
|
2942
2958
|
.app-layout .input-area {
|
|
2943
|
-
background: linear-gradient(0deg, rgba(
|
|
2959
|
+
background: linear-gradient(0deg, rgba(39,41,55,0.98) 0%, rgba(39,41,55,0.78) 64%, transparent 100%);
|
|
2944
2960
|
}
|
|
2945
2961
|
|
|
2946
2962
|
.app-layout .input-box {
|
|
2947
|
-
background: rgba(
|
|
2948
|
-
border-color: rgba(
|
|
2963
|
+
background: rgba(34, 36, 49, 0.94);
|
|
2964
|
+
border-color: rgba(218,225,255,0.15);
|
|
2949
2965
|
box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,0.035);
|
|
2950
2966
|
}
|
|
2951
2967
|
|
|
2952
2968
|
.app-layout .input-box:focus-within {
|
|
2953
|
-
border-color: rgba(
|
|
2954
|
-
box-shadow: var(--shadow), 0 0 0 3px rgba(
|
|
2969
|
+
border-color: rgba(167,124,255,0.42);
|
|
2970
|
+
box-shadow: var(--shadow), 0 0 0 3px rgba(167,124,255,0.08);
|
|
2955
2971
|
}
|
|
2956
2972
|
|
|
2957
2973
|
.app-layout .action-btn:hover,
|
|
2958
2974
|
.app-layout .file-type-btn:hover {
|
|
2959
|
-
background: rgba(
|
|
2975
|
+
background: rgba(167,124,255,0.10);
|
|
2960
2976
|
color: var(--accent);
|
|
2961
|
-
border-color: rgba(
|
|
2977
|
+
border-color: rgba(167,124,255,0.28);
|
|
2962
2978
|
}
|
|
2963
2979
|
|
|
2964
2980
|
.app-layout .send-btn {
|
|
2965
|
-
background: #
|
|
2966
|
-
color: #
|
|
2967
|
-
box-shadow: 0 8px 20px rgba(0,0,0,0.24);
|
|
2981
|
+
background: #f7f7f2;
|
|
2982
|
+
color: #242632;
|
|
2983
|
+
box-shadow: 0 8px 20px rgba(0,0,0,0.24), 0 0 22px rgba(247,247,242,0.12);
|
|
2968
2984
|
}
|
|
2969
2985
|
|
|
2970
2986
|
.app-layout .send-btn:hover {
|
|
2971
|
-
background: #
|
|
2987
|
+
background: #ffffff;
|
|
2972
2988
|
box-shadow: 0 10px 24px rgba(0,0,0,0.28);
|
|
2973
2989
|
}
|
|
2974
2990
|
</style>
|
|
@@ -3864,22 +3880,26 @@
|
|
|
3864
3880
|
|
|
3865
3881
|
function modelOptionHtml(model, group, engine) {
|
|
3866
3882
|
const engineMissing = engine && !engine.installed;
|
|
3883
|
+
const isLocalEngine = engine && (engine.kind === 'local' || engine.kind === 'local-server');
|
|
3884
|
+
const unsupported = engine?.supported === false;
|
|
3867
3885
|
const keyMissing = model.available === false;
|
|
3868
3886
|
const verifyFailed = engine?.kind === 'cloud' && model.verified === false;
|
|
3869
3887
|
const verifyUnknown = engine?.kind === 'cloud' && model.verified == null;
|
|
3870
3888
|
const supportsPull = model.pullable === true;
|
|
3871
3889
|
const needsPull = supportsPull && engine.installed && model.pulled === false;
|
|
3872
|
-
const isUnavailable = engineMissing || keyMissing || verifyFailed;
|
|
3873
|
-
const badge =
|
|
3874
|
-
:
|
|
3890
|
+
const isUnavailable = unsupported || (!isLocalEngine && engineMissing) || keyMissing || verifyFailed;
|
|
3891
|
+
const badge = unsupported ? '현재 환경 미지원'
|
|
3892
|
+
: engineMissing && isLocalEngine ? '설치 후 자동 로드'
|
|
3893
|
+
: engineMissing ? '엔진 설치 필요'
|
|
3894
|
+
: needsPull ? '다운로드 후 자동 로드'
|
|
3875
3895
|
: keyMissing ? `필요: ${model.requires || 'API key'}`
|
|
3876
3896
|
: verifyFailed ? `실패: ${model.verify_reason || '검증 실패'}`
|
|
3877
3897
|
: verifyUnknown ? '실사용 테스트 필요'
|
|
3878
3898
|
: (model.tag || group);
|
|
3879
|
-
const icon = isUnavailable ? 'ti-lock' : needsPull ? 'ti-cloud-download' : verifyUnknown ? 'ti-activity' : 'ti-switch-3';
|
|
3880
|
-
const cls = needsPull ? ' needs-pull' : '';
|
|
3881
|
-
const action =
|
|
3882
|
-
? `
|
|
3899
|
+
const icon = isUnavailable ? 'ti-lock' : (engineMissing || needsPull) ? 'ti-cloud-download' : verifyUnknown ? 'ti-activity' : 'ti-switch-3';
|
|
3900
|
+
const cls = (engineMissing || needsPull) && isLocalEngine ? ' needs-pull' : '';
|
|
3901
|
+
const action = isLocalEngine
|
|
3902
|
+
? `prepareAndLoadModel('${encodeURIComponent(model.id)}', '${engine?.id || ''}')`
|
|
3883
3903
|
: `loadSelectedModel('${encodeURIComponent(model.id)}', '${engine?.id || ''}')`;
|
|
3884
3904
|
return `
|
|
3885
3905
|
<button class="model-option${cls}" ${isUnavailable ? 'disabled' : ''} onclick="${action}">
|
|
@@ -3939,9 +3959,11 @@
|
|
|
3939
3959
|
}
|
|
3940
3960
|
|
|
3941
3961
|
function engineCardHtml(engine) {
|
|
3942
|
-
const
|
|
3962
|
+
const waitingForServer = engine.id === 'lmstudio' && engine.installed && engine.server_ready === false;
|
|
3963
|
+
const unsupported = engine.supported === false;
|
|
3964
|
+
const state = unsupported ? '미지원' : waitingForServer ? '서버 대기' : engine.installed ? '설치됨' : '설치 필요';
|
|
3943
3965
|
const needsApiKey = engine.kind === 'cloud' && !!engine.requires;
|
|
3944
|
-
const installButton = !needsApiKey && engine.installable && !engine.installed
|
|
3966
|
+
const installButton = !unsupported && !needsApiKey && engine.installable && !engine.installed
|
|
3945
3967
|
? `<button class="admin-action" onclick="installEngine('${engine.id}')"><i class="ti ti-download"></i> 설치</button>`
|
|
3946
3968
|
: '';
|
|
3947
3969
|
const requirement = needsApiKey
|
|
@@ -3964,7 +3986,7 @@
|
|
|
3964
3986
|
${engine.note ? `<span>${escapeHtml(engine.note)}</span>` : ''}
|
|
3965
3987
|
</div>
|
|
3966
3988
|
<div class="engine-actions">
|
|
3967
|
-
<span class="engine-state ${engine.installed ? 'ready' : ''}">${state}</span>
|
|
3989
|
+
<span class="engine-state ${engine.installed && !waitingForServer && !unsupported ? 'ready' : ''}">${state}</span>
|
|
3968
3990
|
${installButton}
|
|
3969
3991
|
</div>
|
|
3970
3992
|
</div>
|
|
@@ -4109,19 +4131,21 @@
|
|
|
4109
4131
|
}
|
|
4110
4132
|
}
|
|
4111
4133
|
|
|
4112
|
-
async function
|
|
4134
|
+
async function prepareAndLoadModel(encodedId, engine = '') {
|
|
4113
4135
|
const modelId = decodeURIComponent(encodedId);
|
|
4114
4136
|
const displayName = compactModelName(modelId);
|
|
4115
|
-
document.getElementById('model-list').innerHTML = `<div class="sensitivity-preview"><b>${escapeHtml(displayName)}</b>
|
|
4137
|
+
document.getElementById('model-list').innerHTML = `<div class="sensitivity-preview"><b>${escapeHtml(displayName)}</b> 준비 중입니다...<br>엔진 설치, 모델 다운로드, 서버 시작, 로드까지 자동으로 진행합니다. 첫 실행은 수 분이 걸릴 수 있습니다.</div>`;
|
|
4116
4138
|
try {
|
|
4117
|
-
const
|
|
4139
|
+
const res = await apiFetch('/engines/prepare-model', {
|
|
4118
4140
|
method: 'POST',
|
|
4119
4141
|
headers: { 'Content-Type': 'application/json' },
|
|
4120
|
-
body: JSON.stringify({ model: modelId })
|
|
4142
|
+
body: JSON.stringify({ model: modelId, engine, user_email: currentUserEmail || null })
|
|
4121
4143
|
});
|
|
4122
|
-
const
|
|
4123
|
-
if (!
|
|
4124
|
-
|
|
4144
|
+
const data = await res.json();
|
|
4145
|
+
if (!res.ok) throw new Error(data.detail || '모델 준비에 실패했습니다.');
|
|
4146
|
+
closeModelPanel();
|
|
4147
|
+
await loadModelStatus();
|
|
4148
|
+
addMessage('ai', `<b>${escapeHtml(compactModelName(data.current || modelId))}</b> 로드 되었습니다.`);
|
|
4125
4149
|
} catch (e) {
|
|
4126
4150
|
document.getElementById('model-list').innerHTML = `
|
|
4127
4151
|
<div class="sensitivity-preview">${escapeHtml(e.message)}</div>
|
|
@@ -4130,6 +4154,10 @@
|
|
|
4130
4154
|
}
|
|
4131
4155
|
}
|
|
4132
4156
|
|
|
4157
|
+
async function pullAndLoadModel(encodedId, engine = '') {
|
|
4158
|
+
return prepareAndLoadModel(encodedId, engine);
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4133
4161
|
function fillVpcForm(config) {
|
|
4134
4162
|
if (!config) return;
|
|
4135
4163
|
document.getElementById('vpc-provider').value = config.provider || '';
|