tcmmichaelb139 commited on
Commit
89493ee
·
1 Parent(s): 66feb21

new style + backend updates

Browse files
evolutiontransformer/redis.py CHANGED
@@ -9,14 +9,22 @@ redis_client = Redis.from_url(REDIS_URL, decode_responses=True)
9
 
10
  def add_model_to_session(session_id: str, model_name: str, ttl_seconds: int = 3600):
11
  session_key = f"session:{session_id}:models"
12
- redis_client.sadd(session_key, model_name)
13
- redis_client.expire(session_key, ttl_seconds)
 
 
 
 
 
 
14
 
15
 
16
  def get_session_models(session_id: str):
17
  session_key = f"session:{session_id}:models"
18
- models = redis_client.smembers(session_key)
19
- return list(models)
 
 
20
 
21
 
22
  def save_model_recipe(
 
9
 
10
  def add_model_to_session(session_id: str, model_name: str, ttl_seconds: int = 3600):
11
  session_key = f"session:{session_id}:models"
12
+
13
+ existing_models = get_session_models(session_id)
14
+
15
+ if model_name not in existing_models:
16
+ existing_models.append(model_name)
17
+
18
+ models_json = json.dumps(existing_models)
19
+ redis_client.setex(session_key, ttl_seconds, models_json)
20
 
21
 
22
  def get_session_models(session_id: str):
23
  session_key = f"session:{session_id}:models"
24
+ models_json = redis_client.get(session_key)
25
+ if models_json:
26
+ return json.loads(models_json)
27
+ return []
28
 
29
 
30
  def save_model_recipe(
frontend/src/App.css CHANGED
@@ -1,39 +1 @@
1
  @import "tailwindcss";
2
-
3
- input[type="range"] {
4
- -webkit-appearance: none;
5
- appearance: none;
6
- height: 6px;
7
- border-radius: 3px;
8
- background: #e2e8f0;
9
- outline: none;
10
- }
11
-
12
- input[type="range"]::-webkit-slider-thumb {
13
- -webkit-appearance: none;
14
- appearance: none;
15
- width: 18px;
16
- height: 18px;
17
- border-radius: 50%;
18
- background: #0ea5e9;
19
- cursor: pointer;
20
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
21
- }
22
-
23
- input[type="range"]::-moz-range-thumb {
24
- width: 18px;
25
- height: 18px;
26
- border-radius: 50%;
27
- background: #0ea5e9;
28
- cursor: pointer;
29
- border: none;
30
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
31
- }
32
-
33
- input[type="range"].accent-primary-600::-webkit-slider-thumb {
34
- background: #0284c7;
35
- }
36
-
37
- input[type="range"].accent-primary-600::-moz-range-thumb {
38
- background: #0284c7;
39
- }
 
1
  @import "tailwindcss";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/App.jsx CHANGED
@@ -1,10 +1,8 @@
1
  import { useState, useEffect } from "react";
2
  import "./App.css";
3
- import Header from "./components/Header";
4
- import ModelSelection from "./components/ModelSelection";
5
- import GlobalControls from "./components/GlobalControls";
6
- import LayerRecipe from "./components/LayerRecipe";
7
- import { useAPI } from "./hooks/useAPI";
8
 
9
  function App() {
10
  const [models, setModels] = useState([]);
@@ -14,117 +12,43 @@ function App() {
14
  const [embeddingLambdas, setEmbeddingLambdas] = useState([0.5, 0.5]);
15
  const [linearLambdas, setLinearLambdas] = useState([0.5, 0.5]);
16
  const [mergedName, setMergedName] = useState("merged");
17
- const [isLoading, setIsLoading] = useState(false);
18
- const [result, setResult] = useState(null);
19
  const [numLayers, setNumLayers] = useState(12);
20
 
21
- const { checkTaskStatus, fetchModels, mergeModels } = useAPI();
22
-
23
  useEffect(() => {
24
- const loadModels = async () => {
25
- const taskId = await fetchModels();
26
- if (taskId) {
27
- setTimeout(
28
- () =>
29
- checkTaskStatus(taskId, (result) => {
30
- if (result && Array.isArray(result)) {
31
- setModels(result);
32
- }
33
- }),
34
- 1000
35
- );
36
- }
37
- };
38
-
39
- loadModels();
40
- }, [fetchModels, checkTaskStatus]);
41
-
42
- const initializeLayerRecipe = () => {
43
- const recipe = [];
44
- for (let i = 0; i < numLayers; i++) {
45
- recipe.push([[i, i, 0.5]]);
46
- }
47
- setLayerRecipe(recipe);
48
- };
49
-
50
- const updateLayerWeight = (layerIndex, weight) => {
51
- const newRecipe = [...layerRecipe];
52
- if (!newRecipe[layerIndex]) {
53
- newRecipe[layerIndex] = [[layerIndex, layerIndex, weight]];
54
- } else {
55
- newRecipe[layerIndex][0][2] = weight;
56
- }
57
- setLayerRecipe(newRecipe);
58
- };
59
-
60
- const handleMerge = async () => {
61
- if (!selectedModel1 || !selectedModel2 || layerRecipe.length === 0) {
62
- alert("Please select both models and configure layer recipe");
63
- return;
64
- }
65
-
66
- setIsLoading(true);
67
- setResult(null);
68
-
69
- const mergeData = {
70
- model1_name: selectedModel1,
71
- model2_name: selectedModel2,
72
- layer_recipe: layerRecipe,
73
- embedding_lambdas: embeddingLambdas,
74
- linear_lambdas: linearLambdas,
75
- merged_name: mergedName,
76
- };
77
-
78
- const taskId = await mergeModels(mergeData);
79
- if (taskId) {
80
- checkTaskStatus(taskId, (result) => {
81
- setResult(result);
82
- setIsLoading(false);
83
- });
84
- } else {
85
- setIsLoading(false);
86
- }
87
- };
88
-
89
- const isMergeDisabled =
90
- isLoading || !selectedModel1 || !selectedModel2 || layerRecipe.length === 0;
91
 
92
  return (
93
- <div className="min-h-screen bg-gradient-to-br from-primary-50 to-secondary-50 p-6">
94
- <div className="max-w-6xl mx-auto">
95
- <Header />
96
-
97
- <div className="grid lg:grid-cols-2 gap-8">
98
- <ModelSelection
99
- models={models}
100
- selectedModel1={selectedModel1}
101
- setSelectedModel1={setSelectedModel1}
102
- selectedModel2={selectedModel2}
103
- setSelectedModel2={setSelectedModel2}
104
- numLayers={numLayers}
105
- setNumLayers={setNumLayers}
106
- mergedName={mergedName}
107
- setMergedName={setMergedName}
108
- onInitializeRecipe={initializeLayerRecipe}
109
- />
110
-
111
- <GlobalControls
112
- embeddingLambdas={embeddingLambdas}
113
- setEmbeddingLambdas={setEmbeddingLambdas}
114
- linearLambdas={linearLambdas}
115
- setLinearLambdas={setLinearLambdas}
116
- onMerge={handleMerge}
117
- isLoading={isLoading}
118
- isDisabled={isMergeDisabled}
119
- result={result}
120
- />
121
- </div>
122
-
123
- <LayerRecipe
124
- layerRecipe={layerRecipe}
125
- onUpdateLayerWeight={updateLayerWeight}
126
- />
127
- </div>
128
  </div>
129
  );
130
  }
 
1
  import { useState, useEffect } from "react";
2
  import "./App.css";
3
+ import Options from "./components/Options";
4
+ import Recipe from "./components/Recipe";
5
+ import { setModelLayers } from "./utils/modelCookies";
 
 
6
 
7
  function App() {
8
  const [models, setModels] = useState([]);
 
12
  const [embeddingLambdas, setEmbeddingLambdas] = useState([0.5, 0.5]);
13
  const [linearLambdas, setLinearLambdas] = useState([0.5, 0.5]);
14
  const [mergedName, setMergedName] = useState("merged");
 
 
15
  const [numLayers, setNumLayers] = useState(12);
16
 
 
 
17
  useEffect(() => {
18
+ setModels(["svamp", "tinystories"]);
19
+ setModelLayers("svamp", 24);
20
+ setModelLayers("tinystories", 24);
21
+ }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  return (
24
+ <div className="h-screen bg-gradient-to-br from-primary-50 to-secondary-50 overflow-hidden">
25
+ <Options
26
+ models={models}
27
+ selectedModel1={selectedModel1}
28
+ selectedModel2={selectedModel2}
29
+ setSelectedModel1={setSelectedModel1}
30
+ setSelectedModel2={setSelectedModel2}
31
+ numLayers={numLayers}
32
+ setNumLayers={setNumLayers}
33
+ layerRecipe={layerRecipe}
34
+ embeddingLambdas={embeddingLambdas}
35
+ linearLambdas={linearLambdas}
36
+ setModels={setModels}
37
+ mergedName={mergedName}
38
+ setMergedName={setMergedName}
39
+ />
40
+ <Recipe
41
+ layerRecipe={layerRecipe}
42
+ setLayerRecipe={setLayerRecipe}
43
+ embeddingLambdas={embeddingLambdas}
44
+ setEmbeddingLambdas={setEmbeddingLambdas}
45
+ linearLambdas={linearLambdas}
46
+ setLinearLambdas={setLinearLambdas}
47
+ numLayers={numLayers}
48
+ selectedModel1={selectedModel1}
49
+ selectedModel2={selectedModel2}
50
+ />
51
+ <div className="max-w-6xl mx-auto"></div>
 
 
 
 
 
 
 
52
  </div>
53
  );
54
  }
frontend/src/assets/react.svg DELETED
frontend/src/components/Dropdown.jsx ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect } from "react";
2
+
3
+ const Dropdown = ({
4
+ label,
5
+ selectedValue,
6
+ onSelect,
7
+ options = [],
8
+ placeholder = "Select an option...",
9
+ disabled = false,
10
+ loading = false,
11
+ icon = null,
12
+ className = "",
13
+ dropdownClassName = "",
14
+ optionClassName = "",
15
+ showSearch = false,
16
+ searchPlaceholder = "Search...",
17
+ emptyMessage = "No options available",
18
+ loadingMessage = "Loading...",
19
+ }) => {
20
+ const [isOpen, setIsOpen] = useState(false);
21
+ const [searchTerm, setSearchTerm] = useState("");
22
+ const dropdownRef = useRef(null);
23
+
24
+ useEffect(() => {
25
+ const handleClickOutside = (event) => {
26
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
27
+ setIsOpen(false);
28
+ }
29
+ };
30
+ document.addEventListener("mousedown", handleClickOutside);
31
+ return () => document.removeEventListener("mousedown", handleClickOutside);
32
+ }, []);
33
+
34
+ const filteredOptions = options.filter((option) =>
35
+ option.label
36
+ ? option.label.toLowerCase().includes(searchTerm.toLowerCase())
37
+ : option.toLowerCase().includes(searchTerm.toLowerCase())
38
+ );
39
+
40
+ const displayValue = selectedValue?.label || selectedValue || placeholder;
41
+ const isSelected = selectedValue && selectedValue !== placeholder;
42
+
43
+ return (
44
+ <div className={`relative ${className}`} ref={dropdownRef}>
45
+ {label && (
46
+ <label className="block text-sm font-medium text-secondary-700 mb-2">
47
+ {label}
48
+ </label>
49
+ )}
50
+ <button
51
+ onClick={() => !disabled && setIsOpen(!isOpen)}
52
+ disabled={disabled}
53
+ className={`w-full p-4 rounded-xl text-left transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed bg-white border-2 border-secondary-300 hover:bg-primary-50 hover:shadow-lg ${className}`}
54
+ >
55
+ <div className="flex items-center justify-between">
56
+ <div className="flex items-center space-x-3">
57
+ {icon && <div className="text-lg">{icon}</div>}
58
+ <span
59
+ className={`${
60
+ isSelected
61
+ ? "text-secondary-800 font-medium"
62
+ : "text-secondary-500"
63
+ } ${loading ? "text-secondary-400" : ""}`}
64
+ >
65
+ {loading ? loadingMessage : displayValue}
66
+ </span>
67
+ </div>
68
+ <div className="flex items-center space-x-2">
69
+ {loading && (
70
+ <div className="animate-spin w-4 h-4 border-2 border-primary-500 border-t-transparent rounded-full"></div>
71
+ )}
72
+ <svg
73
+ className={`w-5 h-5 text-primary-600 transition-transform duration-200 ${
74
+ isOpen ? "rotate-180" : ""
75
+ } ${disabled ? "text-secondary-400" : ""}`}
76
+ fill="none"
77
+ stroke="currentColor"
78
+ viewBox="0 0 24 24"
79
+ >
80
+ <path
81
+ strokeLinecap="round"
82
+ strokeLinejoin="round"
83
+ strokeWidth="2"
84
+ d="M19 9l-7 7-7-7"
85
+ />
86
+ </svg>
87
+ </div>
88
+ </div>
89
+ </button>
90
+ {isOpen && !disabled && (
91
+ <div
92
+ className={`absolute z-50 w-full mt-2 rounded-xl max-h-60 overflow-hidden bg-white border-2 border-primary-200 shadow-lg ${dropdownClassName}`}
93
+ >
94
+ {showSearch && (
95
+ <div className="p-3 border-b border-secondary-100">
96
+ <div className="relative">
97
+ <svg
98
+ className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-secondary-400"
99
+ fill="none"
100
+ stroke="currentColor"
101
+ viewBox="0 0 24 24"
102
+ >
103
+ <path
104
+ strokeLinecap="round"
105
+ strokeLinejoin="round"
106
+ strokeWidth="2"
107
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
108
+ />
109
+ </svg>
110
+ <input
111
+ type="text"
112
+ placeholder={searchPlaceholder}
113
+ value={searchTerm}
114
+ onChange={(e) => setSearchTerm(e.target.value)}
115
+ className="w-full pl-9 pr-3 py-2 border border-secondary-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent hover:border-primary-300 transition-colors text-sm"
116
+ onClick={(e) => e.stopPropagation()}
117
+ />
118
+ </div>
119
+ </div>
120
+ )}
121
+ <div className="max-h-48 overflow-y-auto p-2">
122
+ {loading ? (
123
+ <div className="p-4 text-center text-secondary-500">
124
+ <div className="animate-spin w-6 h-6 border-2 border-primary-500 border-t-transparent rounded-full mx-auto mb-2"></div>
125
+ {loadingMessage}
126
+ </div>
127
+ ) : filteredOptions.length === 0 ? (
128
+ <div className="p-4 text-center text-secondary-500">
129
+ {emptyMessage}
130
+ </div>
131
+ ) : (
132
+ filteredOptions.map((option) => {
133
+ const optionValue = option.value || option;
134
+ const optionLabel = option.label || option;
135
+ const optionIcon = option.icon;
136
+ const optionDescription = option.description;
137
+ const isOptionSelected =
138
+ selectedValue === optionValue ||
139
+ (selectedValue?.value && selectedValue.value === optionValue);
140
+ return (
141
+ <button
142
+ key={optionValue}
143
+ onClick={() => {
144
+ onSelect(option);
145
+ setIsOpen(false);
146
+ setSearchTerm("");
147
+ }}
148
+ className={`w-full p-3 text-left rounded-lg transition-all duration-200 hover:bg-blue-100 hover:text-blue-900 ${
149
+ isOptionSelected
150
+ ? "bg-gradient-to-r from-primary-200 to-accent-200 text-primary-800 font-medium"
151
+ : "text-secondary-700 hover:bg-blue-50"
152
+ } ${optionClassName}`}
153
+ >
154
+ <div className="flex items-center space-x-3">
155
+ {optionIcon && (
156
+ <div className="text-lg">{optionIcon}</div>
157
+ )}
158
+ <div
159
+ className={`w-3 h-3 rounded-full transition-colors ${
160
+ isOptionSelected
161
+ ? "bg-primary-600"
162
+ : "bg-secondary-300"
163
+ }`}
164
+ ></div>
165
+ <div className="flex-1 min-w-0">
166
+ <div className="truncate font-medium">
167
+ {optionLabel}
168
+ </div>
169
+ {optionDescription && (
170
+ <div className="text-xs text-secondary-500 truncate mt-1">
171
+ {optionDescription}
172
+ </div>
173
+ )}
174
+ </div>
175
+ {isOptionSelected && (
176
+ <svg
177
+ className="w-4 h-4 text-primary-600"
178
+ fill="none"
179
+ stroke="currentColor"
180
+ viewBox="0 0 24 24"
181
+ >
182
+ <path
183
+ strokeLinecap="round"
184
+ strokeLinejoin="round"
185
+ strokeWidth="2"
186
+ d="M5 13l4 4L19 7"
187
+ />
188
+ </svg>
189
+ )}
190
+ </div>
191
+ </button>
192
+ );
193
+ })
194
+ )}
195
+ </div>
196
+ </div>
197
+ )}
198
+ </div>
199
+ );
200
+ };
201
+
202
+ export default Dropdown;
frontend/src/components/GlobalControls.jsx DELETED
@@ -1,135 +0,0 @@
1
- const GlobalControls = ({
2
- embeddingLambdas,
3
- setEmbeddingLambdas,
4
- linearLambdas,
5
- setLinearLambdas,
6
- onMerge,
7
- isLoading,
8
- isDisabled,
9
- result,
10
- }) => {
11
- return (
12
- <div className="bg-white rounded-2xl shadow-lg p-8 border border-secondary-100">
13
- <h2 className="text-2xl font-semibold text-secondary-800 mb-6">
14
- Global Controls
15
- </h2>
16
-
17
- <div className="space-y-6">
18
- <div>
19
- <label className="block text-sm font-medium text-secondary-700 mb-2">
20
- Embedding Weights
21
- </label>
22
- <div className="grid grid-cols-2 gap-4">
23
- <div>
24
- <span className="text-xs text-secondary-600">Model 1</span>
25
- <input
26
- type="range"
27
- min="0"
28
- max="1"
29
- step="0.05"
30
- value={embeddingLambdas[0]}
31
- onChange={(e) =>
32
- setEmbeddingLambdas([
33
- parseFloat(e.target.value),
34
- 1 - parseFloat(e.target.value),
35
- ])
36
- }
37
- className="w-full"
38
- />
39
- <span className="text-sm text-secondary-600">
40
- {embeddingLambdas[0].toFixed(2)}
41
- </span>
42
- </div>
43
- <div>
44
- <span className="text-xs text-secondary-600">Model 2</span>
45
- <input
46
- type="range"
47
- min="0"
48
- max="1"
49
- step="0.05"
50
- value={embeddingLambdas[1]}
51
- onChange={(e) =>
52
- setEmbeddingLambdas([
53
- 1 - parseFloat(e.target.value),
54
- parseFloat(e.target.value),
55
- ])
56
- }
57
- className="w-full"
58
- />
59
- <span className="text-sm text-secondary-600">
60
- {embeddingLambdas[1].toFixed(2)}
61
- </span>
62
- </div>
63
- </div>
64
- </div>
65
-
66
- <div>
67
- <label className="block text-sm font-medium text-secondary-700 mb-2">
68
- Linear Weights
69
- </label>
70
- <div className="grid grid-cols-2 gap-4">
71
- <div>
72
- <span className="text-xs text-secondary-600">Model 1</span>
73
- <input
74
- type="range"
75
- min="0"
76
- max="1"
77
- step="0.05"
78
- value={linearLambdas[0]}
79
- onChange={(e) =>
80
- setLinearLambdas([
81
- parseFloat(e.target.value),
82
- 1 - parseFloat(e.target.value),
83
- ])
84
- }
85
- className="w-full"
86
- />
87
- <span className="text-sm text-secondary-600">
88
- {linearLambdas[0].toFixed(2)}
89
- </span>
90
- </div>
91
- <div>
92
- <span className="text-xs text-secondary-600">Model 2</span>
93
- <input
94
- type="range"
95
- min="0"
96
- max="1"
97
- step="0.05"
98
- value={linearLambdas[1]}
99
- onChange={(e) =>
100
- setLinearLambdas([
101
- 1 - parseFloat(e.target.value),
102
- parseFloat(e.target.value),
103
- ])
104
- }
105
- className="w-full"
106
- />
107
- <span className="text-sm text-secondary-600">
108
- {linearLambdas[1].toFixed(2)}
109
- </span>
110
- </div>
111
- </div>
112
- </div>
113
-
114
- <button
115
- onClick={onMerge}
116
- disabled={isDisabled}
117
- className="w-full py-4 bg-gradient-to-r from-primary-600 to-accent-600 text-white font-semibold rounded-lg hover:from-primary-700 hover:to-accent-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
118
- >
119
- {isLoading ? "Merging Models..." : "Merge Models"}
120
- </button>
121
-
122
- {result && (
123
- <div className="mt-4 p-4 bg-accent-50 border border-accent-200 rounded-lg">
124
- <h3 className="font-medium text-accent-800 mb-2">
125
- Merge Complete!
126
- </h3>
127
- <p className="text-sm text-accent-700">{JSON.stringify(result)}</p>
128
- </div>
129
- )}
130
- </div>
131
- </div>
132
- );
133
- };
134
-
135
- export default GlobalControls;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/Header.jsx DELETED
@@ -1,15 +0,0 @@
1
- const Header = () => {
2
- return (
3
- <header className="text-center mb-12">
4
- <h1 className="text-5xl font-bold text-secondary-800 mb-4">
5
- Evolution Transformer
6
- </h1>
7
- <p className="text-xl text-secondary-600 max-w-2xl mx-auto">
8
- Combine and evolve transformer models by blending layers with precise
9
- control
10
- </p>
11
- </header>
12
- );
13
- };
14
-
15
- export default Header;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/InferencePopup.jsx ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import Dropdown from "./Dropdown";
3
+ import { useAPI } from "../hooks/useAPI";
4
+
5
+ const InferencePopup = ({ isOpen, onClose, models }) => {
6
+ const [selectedModel, setSelectedModel] = useState("");
7
+ const [prompt, setPrompt] = useState("");
8
+ const [response, setResponse] = useState("");
9
+ const [isLoading, setIsLoading] = useState(false);
10
+ const [error, setError] = useState("");
11
+
12
+ const { inference } = useAPI();
13
+
14
+ const handleInference = async () => {
15
+ if (!selectedModel || !prompt.trim()) {
16
+ setError("Please select a model and enter a prompt");
17
+ return;
18
+ }
19
+
20
+ setIsLoading(true);
21
+ setError("");
22
+ setResponse("");
23
+
24
+ try {
25
+ const inferenceData = {
26
+ model: selectedModel,
27
+ prompt: prompt,
28
+ max_length: 100, // You can make this configurable if needed
29
+ };
30
+
31
+ const result = await inference(inferenceData);
32
+
33
+ if (result && result.generated_text) {
34
+ setResponse(result.generated_text);
35
+ } else if (result && result.error) {
36
+ setError(`Inference failed: ${result.error}`);
37
+ } else {
38
+ setError("No response received from the model");
39
+ }
40
+ } catch (err) {
41
+ setError(`Error: ${err.message}`);
42
+ } finally {
43
+ setIsLoading(false);
44
+ }
45
+ };
46
+
47
+ const handleClose = () => {
48
+ setSelectedModel("");
49
+ setPrompt("");
50
+ setResponse("");
51
+ setError("");
52
+ setIsLoading(false);
53
+ onClose();
54
+ };
55
+
56
+ if (!isOpen) return null;
57
+
58
+ return (
59
+ <div
60
+ className="fixed inset-0 z-50 flex items-center justify-center p-4"
61
+ style={{
62
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
63
+ backdropFilter: "blur(8px)",
64
+ WebkitBackdropFilter: "blur(8px)", // Safari support
65
+ }}
66
+ >
67
+ <div className="bg-white rounded-2xl border-2 border-primary-200 shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
68
+ {/* Header */}
69
+ <div className="p-6 border-b border-secondary-200 bg-gradient-to-r from-primary-50 to-accent-50">
70
+ <div className="flex items-center justify-between">
71
+ <div className="flex items-center space-x-3">
72
+ <div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-accent-500 rounded-lg flex items-center justify-center text-white">
73
+ <svg
74
+ xmlns="http://www.w3.org/2000/svg"
75
+ width="20"
76
+ height="20"
77
+ viewBox="0 0 24 24"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ strokeWidth="2"
81
+ strokeLinecap="round"
82
+ strokeLinejoin="round"
83
+ >
84
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
85
+ </svg>
86
+ </div>
87
+ <h2 className="text-xl font-bold text-secondary-800">
88
+ Model Inference
89
+ </h2>
90
+ </div>
91
+ <button
92
+ onClick={handleClose}
93
+ className="w-8 h-8 rounded-lg hover:bg-secondary-200 transition-colors duration-200 flex items-center justify-center text-secondary-600 hover:text-secondary-800"
94
+ >
95
+ <svg
96
+ xmlns="http://www.w3.org/2000/svg"
97
+ width="20"
98
+ height="20"
99
+ viewBox="0 0 24 24"
100
+ fill="none"
101
+ stroke="currentColor"
102
+ strokeWidth="2"
103
+ strokeLinecap="round"
104
+ strokeLinejoin="round"
105
+ >
106
+ <path d="M18 6L6 18M6 6l12 12" />
107
+ </svg>
108
+ </button>
109
+ </div>
110
+ </div>
111
+
112
+ {/* Content */}
113
+ <div className="p-6 space-y-6 max-h-[calc(90vh-140px)] overflow-y-auto">
114
+ {/* Model Selection */}
115
+ <Dropdown
116
+ label="Select Model"
117
+ selectedValue={selectedModel}
118
+ onSelect={setSelectedModel}
119
+ options={models}
120
+ placeholder="Choose a model for inference..."
121
+ showSearch={true}
122
+ searchPlaceholder="Search models..."
123
+ />
124
+
125
+ {/* Prompt Input */}
126
+ <div>
127
+ <label className="block text-sm font-medium text-secondary-700 mb-2">
128
+ Prompt
129
+ </label>
130
+ <textarea
131
+ value={prompt}
132
+ onChange={(e) => setPrompt(e.target.value)}
133
+ placeholder="Enter your prompt here..."
134
+ className="w-full h-32 p-3 border-2 border-secondary-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none"
135
+ disabled={isLoading}
136
+ />
137
+ </div>
138
+
139
+ {/* Generate Button */}
140
+ <button
141
+ onClick={handleInference}
142
+ disabled={isLoading || !selectedModel || !prompt.trim()}
143
+ className="w-full py-3 px-4 bg-gradient-to-r from-primary-500 to-accent-500 text-white font-medium rounded-lg hover:from-primary-600 hover:to-accent-600 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
144
+ >
145
+ {isLoading ? (
146
+ <>
147
+ <div className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full"></div>
148
+ <span>Generating...</span>
149
+ </>
150
+ ) : (
151
+ <>
152
+ <svg
153
+ xmlns="http://www.w3.org/2000/svg"
154
+ width="16"
155
+ height="16"
156
+ viewBox="0 0 24 24"
157
+ fill="none"
158
+ stroke="currentColor"
159
+ strokeWidth="2"
160
+ strokeLinecap="round"
161
+ strokeLinejoin="round"
162
+ >
163
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
164
+ </svg>
165
+ <span>Generate</span>
166
+ </>
167
+ )}
168
+ </button>
169
+
170
+ {/* Error Display */}
171
+ {error && (
172
+ <div className="p-3 rounded-lg bg-red-100 border border-red-200 text-red-800 text-sm">
173
+ {error}
174
+ </div>
175
+ )}
176
+
177
+ {/* Response Display */}
178
+ {response && (
179
+ <div>
180
+ <label className="block text-sm font-medium text-secondary-700 mb-2">
181
+ Generated Response
182
+ </label>
183
+ <div className="p-4 bg-primary-50 border-2 border-primary-200 rounded-xl">
184
+ <p className="text-secondary-800 whitespace-pre-wrap">
185
+ {response}
186
+ </p>
187
+ </div>
188
+ </div>
189
+ )}
190
+ </div>
191
+ </div>
192
+ </div>
193
+ );
194
+ };
195
+
196
+ export default InferencePopup;
frontend/src/components/LayerRecipe.jsx DELETED
@@ -1,46 +0,0 @@
1
- const LayerRecipe = ({ layerRecipe, onUpdateLayerWeight }) => {
2
- if (layerRecipe.length === 0) return null;
3
-
4
- return (
5
- <div className="mt-8 bg-white rounded-2xl shadow-lg p-8 border border-secondary-100">
6
- <h2 className="text-2xl font-semibold text-secondary-800 mb-6">
7
- Layer Recipe
8
- </h2>
9
-
10
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
11
- {layerRecipe.map((layer, index) => (
12
- <div
13
- key={index}
14
- className="bg-secondary-50 rounded-lg p-4 border border-secondary-200"
15
- >
16
- <div className="text-sm font-medium text-secondary-700 mb-2">
17
- Layer {index}
18
- </div>
19
- <div className="space-y-2">
20
- <input
21
- type="range"
22
- min="0"
23
- max="1"
24
- step="0.05"
25
- value={layer[0]?.[2] || 0.5}
26
- onChange={(e) =>
27
- onUpdateLayerWeight(index, parseFloat(e.target.value))
28
- }
29
- className="w-full accent-primary-600"
30
- />
31
- <div className="flex justify-between text-xs text-secondary-600">
32
- <span>Model 1</span>
33
- <span className="font-medium">
34
- {(layer[0]?.[2] || 0.5).toFixed(2)}
35
- </span>
36
- <span>Model 2</span>
37
- </div>
38
- </div>
39
- </div>
40
- ))}
41
- </div>
42
- </div>
43
- );
44
- };
45
-
46
- export default LayerRecipe;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ModelSelection.jsx DELETED
@@ -1,93 +0,0 @@
1
- const ModelSelection = ({
2
- models,
3
- selectedModel1,
4
- setSelectedModel1,
5
- selectedModel2,
6
- setSelectedModel2,
7
- numLayers,
8
- setNumLayers,
9
- mergedName,
10
- setMergedName,
11
- onInitializeRecipe,
12
- }) => {
13
- return (
14
- <div className="bg-white rounded-2xl shadow-lg p-8 border border-secondary-100">
15
- <h2 className="text-2xl font-semibold text-secondary-800 mb-6">
16
- Model Selection
17
- </h2>
18
-
19
- <div className="space-y-6">
20
- <div>
21
- <label className="block text-sm font-medium text-secondary-700 mb-2">
22
- Base Model (Model 1)
23
- </label>
24
- <select
25
- value={selectedModel1}
26
- onChange={(e) => setSelectedModel1(e.target.value)}
27
- className="w-full p-3 border border-secondary-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
28
- >
29
- <option value="">Select a model</option>
30
- {models.map((model) => (
31
- <option key={model} value={model}>
32
- {model}
33
- </option>
34
- ))}
35
- </select>
36
- </div>
37
-
38
- <div>
39
- <label className="block text-sm font-medium text-secondary-700 mb-2">
40
- Target Model (Model 2)
41
- </label>
42
- <select
43
- value={selectedModel2}
44
- onChange={(e) => setSelectedModel2(e.target.value)}
45
- className="w-full p-3 border border-secondary-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
46
- >
47
- <option value="">Select a model</option>
48
- {models.map((model) => (
49
- <option key={model} value={model}>
50
- {model}
51
- </option>
52
- ))}
53
- </select>
54
- </div>
55
-
56
- <div>
57
- <label className="block text-sm font-medium text-secondary-700 mb-2">
58
- Number of Layers
59
- </label>
60
- <input
61
- type="number"
62
- value={numLayers}
63
- onChange={(e) => setNumLayers(parseInt(e.target.value))}
64
- className="w-full p-3 border border-secondary-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
65
- min="1"
66
- max="48"
67
- />
68
- <button
69
- onClick={onInitializeRecipe}
70
- className="mt-2 px-4 py-2 bg-secondary-600 text-white rounded-lg hover:bg-secondary-700 transition-colors"
71
- >
72
- Initialize Recipe
73
- </button>
74
- </div>
75
-
76
- <div>
77
- <label className="block text-sm font-medium text-secondary-700 mb-2">
78
- Merged Model Name
79
- </label>
80
- <input
81
- type="text"
82
- value={mergedName}
83
- onChange={(e) => setMergedName(e.target.value)}
84
- className="w-full p-3 border border-secondary-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
85
- placeholder="Enter merged model name"
86
- />
87
- </div>
88
- </div>
89
- </div>
90
- );
91
- };
92
-
93
- export default ModelSelection;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/NumberInput.jsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+
3
+ const NumberInput = ({
4
+ label,
5
+ value,
6
+ onChange,
7
+ min = 1,
8
+ max = 48,
9
+ className = "",
10
+ compact = false,
11
+ }) => {
12
+ const [inputValue, setInputValue] = useState(value?.toString() || "");
13
+
14
+ useEffect(() => {
15
+ setInputValue(value?.toString() || "");
16
+ }, [value]);
17
+
18
+ const handleChange = (e) => {
19
+ const inputVal = e.target.value;
20
+ setInputValue(inputVal);
21
+ if (inputVal === "") return;
22
+ const numValue = parseInt(inputVal);
23
+ if (!isNaN(numValue)) {
24
+ const clampedValue = Math.max(min, Math.min(max, numValue));
25
+ onChange(clampedValue);
26
+ if (clampedValue !== numValue) {
27
+ setInputValue(clampedValue.toString());
28
+ }
29
+ }
30
+ };
31
+
32
+ const handleBlur = () => {
33
+ if (inputValue === "" || isNaN(parseInt(inputValue))) {
34
+ onChange(min);
35
+ setInputValue(min.toString());
36
+ }
37
+ };
38
+
39
+ return (
40
+ <div className={className}>
41
+ {label && (
42
+ <label
43
+ className={`block font-medium text-secondary-700 ${
44
+ compact ? "text-xs mb-1" : "text-sm mb-2"
45
+ }`}
46
+ >
47
+ {label}
48
+ </label>
49
+ )}
50
+ <div className="relative">
51
+ <input
52
+ type="number"
53
+ value={inputValue}
54
+ onChange={handleChange}
55
+ onBlur={handleBlur}
56
+ className={`w-full bg-white border-2 border-secondary-300 hover:bg-primary-50 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all duration-200 text-secondary-800 font-medium ${
57
+ compact ? "p-2 rounded-lg text-sm" : "p-4 rounded-xl"
58
+ }`}
59
+ min={min}
60
+ max={max}
61
+ />
62
+ <div
63
+ className={`absolute right-2 top-1/2 transform -translate-y-1/2 ${
64
+ compact ? "right-1" : "right-3"
65
+ }`}
66
+ >
67
+ <div
68
+ className={`text-secondary-500 bg-white px-1 py-0.5 rounded border border-secondary-200 ${
69
+ compact ? "text-xs px-1" : "text-xs px-2 py-1"
70
+ }`}
71
+ >
72
+ {min}-{max}
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ );
78
+ };
79
+
80
+ export default NumberInput;
frontend/src/components/Options.jsx ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import Dropdown from "./Dropdown";
3
+ import NumberInput from "./NumberInput";
4
+ import InferencePopup from "./InferencePopup";
5
+ import { setModelLayers } from "../utils/modelCookies";
6
+ import { useAPI } from "../hooks/useAPI";
7
+
8
+ const Options = ({
9
+ models,
10
+ selectedModel1,
11
+ selectedModel2,
12
+ setSelectedModel1,
13
+ setSelectedModel2,
14
+ numLayers,
15
+ setNumLayers,
16
+ layerRecipe,
17
+ embeddingLambdas,
18
+ linearLambdas,
19
+ setModels,
20
+ mergedName,
21
+ setMergedName,
22
+ }) => {
23
+ const [isLoading, setIsLoading] = useState(false);
24
+ const [mergeStatus, setMergeStatus] = useState("");
25
+ const [isInferenceOpen, setIsInferenceOpen] = useState(false);
26
+ const { mergeModels, checkTaskStatus } = useAPI();
27
+
28
+ const handleMerge = async () => {
29
+ if (
30
+ !selectedModel1 ||
31
+ !selectedModel2 ||
32
+ layerRecipe.length === 0 ||
33
+ !mergedName.trim()
34
+ ) {
35
+ setMergeStatus(
36
+ "Error: Please select models, configure recipe, and provide a merged model name"
37
+ );
38
+ return;
39
+ }
40
+
41
+ setIsLoading(true);
42
+ setMergeStatus("Merging models...");
43
+
44
+ try {
45
+ const mergeData = {
46
+ model1: selectedModel1,
47
+ model2: selectedModel2,
48
+ layer_recipe: layerRecipe,
49
+ embedding_lambdas: embeddingLambdas,
50
+ linear_lambdas: linearLambdas,
51
+ num_layers: numLayers,
52
+ merged_name: mergedName,
53
+ };
54
+
55
+ const taskId = await mergeModels(mergeData);
56
+
57
+ if (taskId) {
58
+ checkTaskStatus(taskId, (result) => {
59
+ if (result.success) {
60
+ setMergeStatus("Merge successful!");
61
+
62
+ const newModelName = result.model_name || mergedName;
63
+ setModels((prev) => [...prev, newModelName]);
64
+
65
+ setModelLayers(newModelName, numLayers);
66
+ } else {
67
+ setMergeStatus(`Merge failed: ${result.error || "Unknown error"}`);
68
+ }
69
+ setIsLoading(false);
70
+ });
71
+ } else {
72
+ setMergeStatus("Failed to start merge process");
73
+ setIsLoading(false);
74
+ }
75
+ } catch (error) {
76
+ setMergeStatus(`Error: ${error.message}`);
77
+ setIsLoading(false);
78
+ }
79
+ };
80
+
81
+ return (
82
+ <div>
83
+ <div className="p-6 border-2 border-primary-200 rounded-2xl bg-gradient-to-br from-white to-primary-50 shadow-xl fixed inset-y-4 left-4 w-[25rem] overflow-y-auto">
84
+ <div className="flex items-center space-x-2 mb-6">
85
+ <div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-accent-500 rounded-lg flex items-center justify-center text-white">
86
+ <svg
87
+ xmlns="http://www.w3.org/2000/svg"
88
+ width="24"
89
+ height="24"
90
+ viewBox="0 0 24 24"
91
+ fill="none"
92
+ stroke="currentColor"
93
+ strokeWidth="2"
94
+ strokeLinecap="round"
95
+ strokeLinejoin="round"
96
+ >
97
+ <path d="M14 17H5" />
98
+ <path d="M19 7h-9" />
99
+ <circle cx="17" cy="17" r="3" />
100
+ <circle cx="7" cy="7" r="3" />
101
+ </svg>
102
+ </div>
103
+ <h2 className="text-xl font-bold text-secondary-800">
104
+ Evolution Transformer Options
105
+ </h2>
106
+ </div>
107
+
108
+ <div className="space-y-6">
109
+ <Dropdown
110
+ label="Model 1"
111
+ selectedValue={selectedModel1}
112
+ onSelect={setSelectedModel1}
113
+ options={models}
114
+ placeholder="Select base model..."
115
+ loading={models.length === 0}
116
+ loadingMessage="Loading models..."
117
+ emptyMessage="No models available"
118
+ showSearch={true}
119
+ searchPlaceholder="Search models..."
120
+ />
121
+
122
+ <Dropdown
123
+ label="Model 2"
124
+ selectedValue={selectedModel2}
125
+ onSelect={setSelectedModel2}
126
+ options={models}
127
+ placeholder="Select target model..."
128
+ loading={models.length === 0}
129
+ loadingMessage="Loading models..."
130
+ emptyMessage="No models available"
131
+ showSearch={true}
132
+ searchPlaceholder="Search models..."
133
+ />
134
+
135
+ <NumberInput
136
+ label="Number of Layers"
137
+ value={numLayers}
138
+ onChange={setNumLayers}
139
+ min={1}
140
+ max={48}
141
+ className="w-full"
142
+ />
143
+
144
+ <div>
145
+ <label className="block text-sm font-medium text-secondary-700 mb-1">
146
+ Merged Model Name
147
+ </label>
148
+ <input
149
+ type="text"
150
+ value={mergedName}
151
+ onChange={(e) => setMergedName(e.target.value)}
152
+ placeholder="Enter merged model name..."
153
+ className="w-full px-3 py-2 border-2 border-secondary-200 rounded-lg focus:border-primary-500 focus:ring-2 focus:ring-primary-200 text-secondary-800 placeholder-secondary-400 transition-all duration-200"
154
+ />
155
+ </div>
156
+
157
+ <div className="p-4 rounded-xl border-2 border-secondary-200 bg-secondary-50">
158
+ <div className="flex items-center space-x-2">
159
+ <div
160
+ className={`w-2 h-2 rounded-full ${
161
+ selectedModel1 && selectedModel2
162
+ ? "bg-green-500"
163
+ : "bg-red-500"
164
+ }`}
165
+ ></div>
166
+ <span>
167
+ Selected:{" "}
168
+ {selectedModel1 && selectedModel2
169
+ ? "Ready to merge"
170
+ : "Incomplete"}
171
+ </span>
172
+ </div>
173
+ </div>
174
+
175
+ <div className="flex space-x-3">
176
+ <div className="flex-1 p-4 rounded-xl border-2 border-accent-200 bg-gradient-to-br from-accent-50 to-primary-50">
177
+ <div className="flex items-center space-x-2 mb-3">
178
+ <div className="w-6 h-6 bg-gradient-to-br from-accent-500 to-secondary-500 rounded-lg flex items-center justify-center text-white">
179
+ <svg
180
+ xmlns="http://www.w3.org/2000/svg"
181
+ width="16"
182
+ height="16"
183
+ viewBox="0 0 24 24"
184
+ fill="none"
185
+ stroke="currentColor"
186
+ strokeWidth="2"
187
+ strokeLinecap="round"
188
+ strokeLinejoin="round"
189
+ >
190
+ <path d="m8 6 4-4 4 4" />
191
+ <path d="M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22" />
192
+ <path d="m20 22-5-5" />
193
+ </svg>
194
+ </div>
195
+ <h3 className="text-sm font-semibold text-secondary-800">
196
+ Merge Models
197
+ </h3>
198
+ </div>
199
+
200
+ <button
201
+ onClick={handleMerge}
202
+ disabled={
203
+ isLoading ||
204
+ !selectedModel1 ||
205
+ !selectedModel2 ||
206
+ layerRecipe.length === 0 ||
207
+ !mergedName.trim()
208
+ }
209
+ className="w-full py-2.5 px-3 bg-gradient-to-r from-accent-500 to-primary-500 text-white font-medium rounded-lg hover:from-accent-600 hover:to-primary-600 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2 text-sm"
210
+ >
211
+ {isLoading ? (
212
+ <>
213
+ <div className="animate-spin w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full"></div>
214
+ <span>Merging...</span>
215
+ </>
216
+ ) : (
217
+ <>
218
+ <svg
219
+ xmlns="http://www.w3.org/2000/svg"
220
+ width="14"
221
+ height="14"
222
+ viewBox="0 0 24 24"
223
+ fill="none"
224
+ stroke="currentColor"
225
+ strokeWidth="2"
226
+ strokeLinecap="round"
227
+ strokeLinejoin="round"
228
+ >
229
+ <path d="m8 6 4-4 4 4" />
230
+ <path d="M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22" />
231
+ <path d="m20 22-5-5" />
232
+ </svg>
233
+ <span>Merge</span>
234
+ </>
235
+ )}
236
+ </button>
237
+ </div>
238
+
239
+ <div className="flex-1 p-4 rounded-xl border-2 border-primary-200 bg-gradient-to-br from-primary-50 to-accent-50">
240
+ <div className="flex items-center space-x-2 mb-3">
241
+ <div className="w-6 h-6 bg-gradient-to-br from-secondary-500 to-primary-500 rounded-lg flex items-center justify-center text-white">
242
+ <svg
243
+ xmlns="http://www.w3.org/2000/svg"
244
+ width="16"
245
+ height="16"
246
+ viewBox="0 0 24 24"
247
+ fill="none"
248
+ stroke="currentColor"
249
+ strokeWidth="2"
250
+ strokeLinecap="round"
251
+ strokeLinejoin="round"
252
+ >
253
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
254
+ </svg>
255
+ </div>
256
+ <h3 className="text-sm font-semibold text-secondary-800">
257
+ Test Inference
258
+ </h3>
259
+ </div>
260
+
261
+ <button
262
+ onClick={() => setIsInferenceOpen(true)}
263
+ className="w-full py-2.5 px-3 bg-gradient-to-r from-secondary-500 to-primary-500 text-white font-medium rounded-lg hover:from-secondary-600 hover:to-primary-600 transition-all duration-200 flex items-center justify-center space-x-2 text-sm"
264
+ >
265
+ <svg
266
+ xmlns="http://www.w3.org/2000/svg"
267
+ width="14"
268
+ height="14"
269
+ viewBox="0 0 24 24"
270
+ fill="none"
271
+ stroke="currentColor"
272
+ strokeWidth="2"
273
+ strokeLinecap="round"
274
+ strokeLinejoin="round"
275
+ >
276
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
277
+ </svg>
278
+ <span>Inference</span>
279
+ </button>
280
+ </div>
281
+ </div>
282
+
283
+ {mergeStatus && (
284
+ <div
285
+ className={`p-2 rounded-lg text-sm font-medium ${
286
+ mergeStatus.includes("successful")
287
+ ? "bg-green-100 text-green-800"
288
+ : mergeStatus.includes("Error") ||
289
+ mergeStatus.includes("failed")
290
+ ? "bg-red-100 text-red-800"
291
+ : "bg-blue-100 text-blue-800"
292
+ }`}
293
+ >
294
+ {mergeStatus}
295
+ </div>
296
+ )}
297
+ </div>
298
+ </div>
299
+
300
+ <InferencePopup
301
+ isOpen={isInferenceOpen}
302
+ onClose={() => setIsInferenceOpen(false)}
303
+ models={models}
304
+ />
305
+ </div>
306
+ );
307
+ };
308
+
309
+ export default Options;
frontend/src/components/Recipe.jsx ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import Dropdown from "./Dropdown";
3
+ import NumberInput from "./NumberInput";
4
+ import {
5
+ getModelLayerCounts,
6
+ initializeDefaultCookies,
7
+ } from "../utils/modelCookies";
8
+
9
+ const Recipe = ({
10
+ layerRecipe,
11
+ setLayerRecipe,
12
+ embeddingLambdas,
13
+ setEmbeddingLambdas,
14
+ linearLambdas,
15
+ setLinearLambdas,
16
+ numLayers,
17
+ selectedModel1,
18
+ selectedModel2,
19
+ }) => {
20
+ const [modelLayerCounts, setModelLayerCounts] = useState({
21
+ model1: 12,
22
+ model2: 12,
23
+ });
24
+ const [expandedBlock, setExpandedBlock] = useState(null);
25
+
26
+ useEffect(() => {
27
+ initializeDefaultCookies(selectedModel1, selectedModel2);
28
+ const counts = getModelLayerCounts(selectedModel1, selectedModel2);
29
+ setModelLayerCounts(counts);
30
+ }, [selectedModel1, selectedModel2]);
31
+
32
+ const initializeLayerRecipe = useCallback(() => {
33
+ const recipe = [];
34
+ for (let i = 0; i < numLayers; i++) {
35
+ recipe.push([[1, 1, 0.5]]);
36
+ }
37
+ setLayerRecipe(recipe);
38
+ }, [numLayers, setLayerRecipe]);
39
+
40
+ useEffect(() => {
41
+ if (layerRecipe.length !== numLayers) {
42
+ initializeLayerRecipe();
43
+ }
44
+ }, [numLayers, layerRecipe.length, initializeLayerRecipe]);
45
+
46
+ const addBlockToLayer = (layerIndex) => {
47
+ const newRecipe = [...layerRecipe];
48
+ const newBlock = [1, 1, 0.5];
49
+ newRecipe[layerIndex] = [...newRecipe[layerIndex], newBlock];
50
+ setLayerRecipe(newRecipe);
51
+ };
52
+
53
+ const removeBlockFromLayer = (layerIndex, blockIndex) => {
54
+ const newRecipe = [...layerRecipe];
55
+ if (newRecipe[layerIndex].length > 1) {
56
+ newRecipe[layerIndex] = newRecipe[layerIndex].filter(
57
+ (_, i) => i !== blockIndex
58
+ );
59
+ setLayerRecipe(newRecipe);
60
+ }
61
+ };
62
+
63
+ const updateBlock = (layerIndex, blockIndex, field, value) => {
64
+ const newRecipe = [...layerRecipe];
65
+ const block = [...newRecipe[layerIndex][blockIndex]];
66
+
67
+ if (field === "model") {
68
+ block[0] = value;
69
+ block[1] = 1;
70
+ } else if (field === "sourceLayer") {
71
+ block[1] = value;
72
+ } else if (field === "percentage") {
73
+ block[2] = value / 100;
74
+ }
75
+
76
+ newRecipe[layerIndex][blockIndex] = block;
77
+ setLayerRecipe(newRecipe);
78
+ };
79
+
80
+ const updateEmbeddingLambda = (index, value) => {
81
+ const newLambdas = [...embeddingLambdas];
82
+ newLambdas[index] = value / 100;
83
+ setEmbeddingLambdas(newLambdas);
84
+ };
85
+
86
+ const updateLinearLambda = (index, value) => {
87
+ const newLambdas = [...linearLambdas];
88
+ newLambdas[index] = value / 100;
89
+ setLinearLambdas(newLambdas);
90
+ };
91
+
92
+ const modelOptions = [
93
+ { value: 1, label: "Model 1" },
94
+ { value: 2, label: "Model 2" },
95
+ ];
96
+
97
+ const getModelName = (modelValue) => {
98
+ return modelValue === 1 ? "Model 1" : "Model 2";
99
+ };
100
+
101
+ const getBlockId = (layerIndex, blockIndex) => {
102
+ return `${layerIndex}-${blockIndex}`;
103
+ };
104
+
105
+ const toggleBlockExpanded = (layerIndex, blockIndex) => {
106
+ const blockId = getBlockId(layerIndex, blockIndex);
107
+ setExpandedBlock(expandedBlock === blockId ? null : blockId);
108
+ };
109
+
110
+ return (
111
+ <div className="p-6 border-2 border-primary-200 rounded-2xl bg-gradient-to-br from-white to-primary-50 shadow-xl fixed top-4 right-4 bottom-4 left-[27rem] overflow-y-auto ">
112
+ <div className="flex items-center space-x-2 mb-6">
113
+ <div className="w-8 h-8 bg-gradient-to-br from-accent-500 to-primary-500 rounded-lg flex items-center justify-center">
114
+ <svg
115
+ xmlns="http://www.w3.org/2000/svg"
116
+ width="20"
117
+ height="20"
118
+ viewBox="0 0 24 24"
119
+ fill="none"
120
+ stroke="currentColor"
121
+ strokeWidth="2"
122
+ strokeLinecap="round"
123
+ strokeLinejoin="round"
124
+ className="text-white"
125
+ >
126
+ <path d="M12 3v18m9-9H3" />
127
+ </svg>
128
+ </div>
129
+ <h2 className="text-xl font-bold text-secondary-800">Layer Recipe</h2>
130
+ </div>
131
+
132
+ <div className="space-y-8">
133
+ <div className="grid grid-cols-2 gap-6">
134
+ <div className="space-y-4">
135
+ <h3 className="text-lg font-semibold text-secondary-700">
136
+ Embedding Layers
137
+ </h3>
138
+ <div className="space-y-3">
139
+ <NumberInput
140
+ label="Token Embedding (%)"
141
+ value={Math.round(embeddingLambdas[0] * 100)}
142
+ onChange={(value) => updateEmbeddingLambda(0, value)}
143
+ min={0}
144
+ max={100}
145
+ />
146
+ <NumberInput
147
+ label="Positional Embedding (%)"
148
+ value={Math.round(embeddingLambdas[1] * 100)}
149
+ onChange={(value) => updateEmbeddingLambda(1, value)}
150
+ min={0}
151
+ max={100}
152
+ />
153
+ </div>
154
+ </div>
155
+
156
+ <div className="space-y-4">
157
+ <h3 className="text-lg font-semibold text-secondary-700">
158
+ Linear Layers
159
+ </h3>
160
+ <div className="space-y-3">
161
+ <NumberInput
162
+ label="LM Head (%)"
163
+ value={Math.round(linearLambdas[0] * 100)}
164
+ onChange={(value) => updateLinearLambda(0, value)}
165
+ min={0}
166
+ max={100}
167
+ />
168
+ <NumberInput
169
+ label="LM Final (%)"
170
+ value={Math.round(linearLambdas[1] * 100)}
171
+ onChange={(value) => updateLinearLambda(1, value)}
172
+ min={0}
173
+ max={100}
174
+ />
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <div className="space-y-4">
180
+ <div className="flex items-center justify-between">
181
+ <h3 className="text-lg font-semibold text-secondary-700">
182
+ Transformer Layers
183
+ </h3>
184
+ <div className="text-xs text-secondary-500 space-x-4">
185
+ <span>
186
+ Model 1:{" "}
187
+ {modelLayerCounts.model1 === "N/A"
188
+ ? "N/A layers"
189
+ : `${modelLayerCounts.model1} layers`}
190
+ </span>
191
+ <span>
192
+ Model 2:{" "}
193
+ {modelLayerCounts.model2 === "N/A"
194
+ ? "N/A layers"
195
+ : `${modelLayerCounts.model2} layers`}
196
+ </span>
197
+ </div>
198
+ </div>
199
+ <div className="flex-1 overflow-y-auto space-y-3">
200
+ {layerRecipe.map((layer, layerIndex) => (
201
+ <div
202
+ key={layerIndex}
203
+ className="relative border-2 border-secondary-200 rounded-xl p-4 bg-white hover:border-primary-300 transition-all duration-200"
204
+ >
205
+ <div className="flex items-center justify-between mb-3">
206
+ <div className="flex items-center space-x-3">
207
+ <h4 className="font-medium text-secondary-700">
208
+ Layer {layerIndex + 1}
209
+ </h4>
210
+ <div className="text-xs text-secondary-500 bg-secondary-100 px-2 py-1 rounded-md">
211
+ Total:{" "}
212
+ {Math.round(
213
+ layer.reduce((sum, block) => sum + block[2], 0) * 100
214
+ )}
215
+ %
216
+ </div>
217
+ </div>
218
+ <button
219
+ onClick={() => addBlockToLayer(layerIndex)}
220
+ className="w-6 h-6 bg-primary-500 text-white rounded-md hover:bg-primary-600 transition-colors duration-200 flex items-center justify-center"
221
+ title="Add block"
222
+ >
223
+ <svg
224
+ xmlns="http://www.w3.org/2000/svg"
225
+ width="12"
226
+ height="12"
227
+ viewBox="0 0 24 24"
228
+ fill="none"
229
+ stroke="currentColor"
230
+ strokeWidth="2"
231
+ strokeLinecap="round"
232
+ strokeLinejoin="round"
233
+ >
234
+ <path d="M12 5v14m7-7H5" />
235
+ </svg>
236
+ </button>
237
+ </div>
238
+
239
+ <div className="flex flex-wrap gap-2">
240
+ {layer.map((block, blockIndex) => {
241
+ const blockId = getBlockId(layerIndex, blockIndex);
242
+ const isExpanded = expandedBlock === blockId;
243
+ const modelName = getModelName(block[0]);
244
+
245
+ return (
246
+ <div key={blockIndex} className="relative">
247
+ <button
248
+ onClick={() =>
249
+ toggleBlockExpanded(layerIndex, blockIndex)
250
+ }
251
+ onContextMenu={(e) => {
252
+ e.preventDefault();
253
+ removeBlockFromLayer(layerIndex, blockIndex);
254
+ }}
255
+ className="px-3 py-2 bg-white border-2 border-secondary-300 rounded-lg hover:border-primary-300 hover:bg-primary-50 transition-all duration-200 text-sm font-medium text-secondary-700 flex items-center space-x-2"
256
+ title="Left click to edit, Right click to delete"
257
+ >
258
+ <span className="text-primary-600">{modelName}</span>
259
+ <span className="text-secondary-500">
260
+ L{block[1]}
261
+ </span>
262
+ <span className="text-accent-600">
263
+ {Math.round(block[2] * 100)}%
264
+ </span>
265
+ <svg
266
+ className={`w-4 h-4 text-secondary-400 transition-transform duration-200 ${
267
+ isExpanded ? "rotate-180" : ""
268
+ }`}
269
+ fill="none"
270
+ stroke="currentColor"
271
+ viewBox="0 0 24 24"
272
+ >
273
+ <path
274
+ strokeLinecap="round"
275
+ strokeLinejoin="round"
276
+ strokeWidth="2"
277
+ d="M19 9l-7 7-7-7"
278
+ />
279
+ </svg>
280
+ </button>
281
+
282
+ {isExpanded && (
283
+ <div
284
+ className="absolute top-full left-0 mt-1 p-3 bg-white border-2 border-primary-200 rounded-lg shadow-lg z-10 min-w-64"
285
+ data-block-id={blockId}
286
+ >
287
+ <div className="space-y-3">
288
+ <div>
289
+ <label className="block text-xs font-medium text-secondary-600 mb-1">
290
+ Model
291
+ </label>
292
+ <Dropdown
293
+ selectedValue={modelOptions.find(
294
+ (opt) => opt.value === block[0]
295
+ )}
296
+ onSelect={(option) => {
297
+ updateBlock(
298
+ layerIndex,
299
+ blockIndex,
300
+ "model",
301
+ option.value
302
+ );
303
+ }}
304
+ options={modelOptions}
305
+ placeholder="Select model..."
306
+ className="w-full"
307
+ />
308
+ </div>
309
+
310
+ <div className="grid grid-cols-2 gap-3">
311
+ <div>
312
+ <label className="block text-xs font-medium text-secondary-600 mb-1">
313
+ Layer
314
+ </label>
315
+ <NumberInput
316
+ value={block[1]}
317
+ onChange={(value) =>
318
+ updateBlock(
319
+ layerIndex,
320
+ blockIndex,
321
+ "sourceLayer",
322
+ value
323
+ )
324
+ }
325
+ min={1}
326
+ max={
327
+ block[0] === 1
328
+ ? modelLayerCounts.model1 === "N/A"
329
+ ? 1
330
+ : modelLayerCounts.model1
331
+ : modelLayerCounts.model2 === "N/A"
332
+ ? 1
333
+ : modelLayerCounts.model2
334
+ }
335
+ compact={true}
336
+ />
337
+ </div>
338
+
339
+ <div>
340
+ <label className="block text-xs font-medium text-secondary-600 mb-1">
341
+ Weight (%)
342
+ </label>
343
+ <NumberInput
344
+ value={Math.round(block[2] * 100)}
345
+ onChange={(value) =>
346
+ updateBlock(
347
+ layerIndex,
348
+ blockIndex,
349
+ "percentage",
350
+ value
351
+ )
352
+ }
353
+ min={0}
354
+ max={100}
355
+ compact={true}
356
+ />
357
+ </div>
358
+ </div>
359
+
360
+ {layer.length > 1 && (
361
+ <button
362
+ onClick={() => {
363
+ removeBlockFromLayer(
364
+ layerIndex,
365
+ blockIndex
366
+ );
367
+ setExpandedBlock(null);
368
+ }}
369
+ className="w-full px-3 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors duration-200 text-sm font-medium"
370
+ >
371
+ Remove Block
372
+ </button>
373
+ )}
374
+ </div>
375
+ </div>
376
+ )}
377
+ </div>
378
+ );
379
+ })}
380
+ </div>
381
+ </div>
382
+ ))}
383
+ </div>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ );
388
+ };
389
+
390
+ export default Recipe;
frontend/src/hooks/useAPI.js CHANGED
@@ -47,9 +47,25 @@ export const useAPI = () => {
47
  }
48
  }, []);
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  return {
51
  checkTaskStatus,
52
  fetchModels,
53
  mergeModels,
 
54
  };
55
  };
 
47
  }
48
  }, []);
49
 
50
+ const inference = useCallback(async (inferenceData) => {
51
+ try {
52
+ const response = await fetch(`${API_BASE}/generate`, {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify(inferenceData),
56
+ });
57
+ const data = await response.json();
58
+ return data;
59
+ } catch (error) {
60
+ console.error("Inference failed:", error);
61
+ return null;
62
+ }
63
+ }, []);
64
+
65
  return {
66
  checkTaskStatus,
67
  fetchModels,
68
  mergeModels,
69
+ inference,
70
  };
71
  };
frontend/src/index.css CHANGED
@@ -1,5 +1,40 @@
1
  @import "tailwindcss";
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  :root {
4
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu,
5
  Cantarell, sans-serif;
@@ -15,5 +50,35 @@
15
  body {
16
  margin: 0;
17
  padding: 0;
18
- min-height: 100vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
 
1
  @import "tailwindcss";
2
 
3
+ @theme {
4
+ --color-primary-50: #f0f9ff;
5
+ --color-primary-100: #e0f2fe;
6
+ --color-primary-200: #bae6fd;
7
+ --color-primary-300: #7dd3fc;
8
+ --color-primary-400: #38bdf8;
9
+ --color-primary-500: #0ea5e9;
10
+ --color-primary-600: #0284c7;
11
+ --color-primary-700: #0369a1;
12
+ --color-primary-800: #075985;
13
+ --color-primary-900: #0c4a6e;
14
+
15
+ --color-secondary-50: #f8fafc;
16
+ --color-secondary-100: #f1f5f9;
17
+ --color-secondary-200: #e2e8f0;
18
+ --color-secondary-300: #cbd5e1;
19
+ --color-secondary-400: #94a3b8;
20
+ --color-secondary-500: #64748b;
21
+ --color-secondary-600: #475569;
22
+ --color-secondary-700: #334155;
23
+ --color-secondary-800: #1e293b;
24
+ --color-secondary-900: #0f172a;
25
+
26
+ --color-accent-50: #f0fdf4;
27
+ --color-accent-100: #dcfce7;
28
+ --color-accent-200: #bbf7d0;
29
+ --color-accent-300: #86efac;
30
+ --color-accent-400: #4ade80;
31
+ --color-accent-500: #22c55e;
32
+ --color-accent-600: #16a34a;
33
+ --color-accent-700: #15803d;
34
+ --color-accent-800: #166534;
35
+ --color-accent-900: #14532d;
36
+ }
37
+
38
  :root {
39
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu,
40
  Cantarell, sans-serif;
 
50
  body {
51
  margin: 0;
52
  padding: 0;
53
+ }
54
+
55
+ /* Custom Scrollbar Styling */
56
+ ::-webkit-scrollbar {
57
+ width: 8px;
58
+ height: 8px;
59
+ }
60
+
61
+ ::-webkit-scrollbar-track {
62
+ background: transparent;
63
+ border-radius: 4px;
64
+ }
65
+
66
+ ::-webkit-scrollbar-thumb {
67
+ background: linear-gradient(135deg, #0ea5e9, #22c55e);
68
+ border-radius: 4px;
69
+ transition: background 0.2s ease;
70
+ }
71
+
72
+ ::-webkit-scrollbar-thumb:hover {
73
+ background: linear-gradient(135deg, #0284c7, #16a34a);
74
+ }
75
+
76
+ ::-webkit-scrollbar-corner {
77
+ background: transparent;
78
+ }
79
+
80
+ /* Firefox scrollbar styling */
81
+ * {
82
+ scrollbar-width: thin;
83
+ scrollbar-color: #0ea5e9 transparent;
84
  }
frontend/src/utils/modelCookies.js ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const getModelLayers = (modelKey) => {
2
+ const cookieValue = document.cookie
3
+ .split("; ")
4
+ .find((row) => row.startsWith(`${modelKey}_layers=`))
5
+ ?.split("=")[1];
6
+
7
+ return cookieValue ? parseInt(cookieValue) : null;
8
+ };
9
+
10
+ export const setModelLayers = (modelKey, numLayers) => {
11
+ document.cookie = `${modelKey}_layers=${numLayers}`;
12
+ };
13
+
14
+ /**
15
+ * Get layer counts using actual model names as keys
16
+ * Returns "N/A" if no model is selected, otherwise returns stored count or default
17
+ */
18
+ export const getModelLayerCounts = (selectedModel1, selectedModel2) => {
19
+ const model1Layers = selectedModel1 ? getModelLayers(selectedModel1) : null;
20
+ const model2Layers = selectedModel2 ? getModelLayers(selectedModel2) : null;
21
+
22
+ return {
23
+ model1: !selectedModel1 ? "N/A" : model1Layers !== null ? model1Layers : 12,
24
+ model2: !selectedModel2 ? "N/A" : model2Layers !== null ? model2Layers : 12,
25
+ };
26
+ };
27
+
28
+ /**
29
+ * Set layer count for a model using its actual name as the key
30
+ */
31
+ export const setModelLayersByName = (modelName, numLayers) => {
32
+ if (modelName) {
33
+ setModelLayers(modelName, numLayers);
34
+ }
35
+ };
36
+
37
+ /**
38
+ * Initialize default layer counts using actual model names as keys
39
+ */
40
+ export const initializeDefaultCookies = (selectedModel1, selectedModel2) => {
41
+ if (selectedModel1 && getModelLayers(selectedModel1) === null) {
42
+ setModelLayers(selectedModel1, 24);
43
+ }
44
+ if (selectedModel2 && getModelLayers(selectedModel2) === null) {
45
+ setModelLayers(selectedModel2, 36);
46
+ }
47
+ };
frontend/tailwind.config.js DELETED
@@ -1,47 +0,0 @@
1
- /** @type {import('tailwindcss').Config} */
2
- export default {
3
- content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4
- theme: {
5
- extend: {
6
- colors: {
7
- primary: {
8
- 50: "#f0f9ff",
9
- 100: "#e0f2fe",
10
- 200: "#bae6fd",
11
- 300: "#7dd3fc",
12
- 400: "#38bdf8",
13
- 500: "#0ea5e9",
14
- 600: "#0284c7",
15
- 700: "#0369a1",
16
- 800: "#075985",
17
- 900: "#0c4a6e",
18
- },
19
- secondary: {
20
- 50: "#f8fafc",
21
- 100: "#f1f5f9",
22
- 200: "#e2e8f0",
23
- 300: "#cbd5e1",
24
- 400: "#94a3b8",
25
- 500: "#64748b",
26
- 600: "#475569",
27
- 700: "#334155",
28
- 800: "#1e293b",
29
- 900: "#0f172a",
30
- },
31
- accent: {
32
- 50: "#f0fdf4",
33
- 100: "#dcfce7",
34
- 200: "#bbf7d0",
35
- 300: "#86efac",
36
- 400: "#4ade80",
37
- 500: "#22c55e",
38
- 600: "#16a34a",
39
- 700: "#15803d",
40
- 800: "#166534",
41
- 900: "#14532d",
42
- },
43
- },
44
- },
45
- },
46
- plugins: [],
47
- };