Spaces:
Sleeping
Sleeping
Commit
·
89493ee
1
Parent(s):
66feb21
new style + backend updates
Browse files- evolutiontransformer/redis.py +12 -4
- frontend/src/App.css +0 -38
- frontend/src/App.jsx +35 -111
- frontend/src/assets/react.svg +0 -1
- frontend/src/components/Dropdown.jsx +202 -0
- frontend/src/components/GlobalControls.jsx +0 -135
- frontend/src/components/Header.jsx +0 -15
- frontend/src/components/InferencePopup.jsx +196 -0
- frontend/src/components/LayerRecipe.jsx +0 -46
- frontend/src/components/ModelSelection.jsx +0 -93
- frontend/src/components/NumberInput.jsx +80 -0
- frontend/src/components/Options.jsx +309 -0
- frontend/src/components/Recipe.jsx +390 -0
- frontend/src/hooks/useAPI.js +16 -0
- frontend/src/index.css +66 -1
- frontend/src/utils/modelCookies.js +47 -0
- frontend/tailwind.config.js +0 -47
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 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
def get_session_models(session_id: str):
|
| 17 |
session_key = f"session:{session_id}:models"
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
| 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
|
| 4 |
-
import
|
| 5 |
-
import
|
| 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 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 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="
|
| 94 |
-
<
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|