From a23ba65fd93b476007f50af49a8e882d0fd187ca Mon Sep 17 00:00:00 2001 From: aman-pandey-bin <160474053+aman-pandey-bin@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:36:15 +0530 Subject: [PATCH 1/3] Add frontend and backend --- .gitignore | 13 +- app.py | 329 +++++++++++++++---------------------------- characters.json | 23 +++ characters.py | 61 +++++++- datasets.json | 21 +++ static/index.js | 296 ++++++++++++++++++++++++++++++++++++++ static/styles.css | 176 +++++++++++++++++++++++ templates/index.html | 47 +++++++ utils.py | 5 + 9 files changed, 747 insertions(+), 224 deletions(-) create mode 100644 characters.json create mode 100644 static/index.js create mode 100644 static/styles.css create mode 100644 templates/index.html diff --git a/.gitignore b/.gitignore index ee526cf..6a3dd85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ __pycache__ eval -.env \ No newline at end of file +.env +.idea/ +.vscode/ +.venv*/ +venv*/ +env*/ +__pycache__/ +dist/ +.coverage* +htmlcov/ +.tox/ +docs/_build/ \ No newline at end of file diff --git a/app.py b/app.py index 70e637b..89c707a 100644 --- a/app.py +++ b/app.py @@ -1,241 +1,138 @@ -import streamlit as st -import json -from characters import characters +from flask import Flask, request, jsonify, render_template +from flask_cors import CORS +from characters import characters, save_character, delete_character, get_character, update_character from utils import Character, Evaluator from datasets import datasets, save_dataset, delete_dataset -def save_character(name, backstory, model, temperature, max_tokens): - characters[name] = { - "backstory": backstory, - "model": model, - "temperature": temperature, - "max_tokens": max_tokens - } - with open("characters.py", "w") as f: - f.write(f"characters = {json.dumps(characters, indent=4)}") +app = Flask(__name__) +CORS(app) -def delete_character(name): - if name in characters: - del characters[name] - with open("characters.py", "w") as f: - f.write(f"characters = {json.dumps(characters, indent=4)}") - st.success(f"Character '{name}' deleted successfully!") - if 'selected_character' in st.session_state and st.session_state.selected_character == name: - del st.session_state.selected_character - st.rerun() +@app.route('/api/characters', methods=['GET']) +def get_characters(): + return jsonify(characters) -def main(): - st.set_page_config(layout="wide") - st.title("AI Character Evaluation Framework") - - # Sidebar - st.sidebar.title("Navigation") - selected_tab = st.sidebar.radio("", ["Characters", "Datasets", "Run Evaluation"]) +@app.route('/api/characters', methods=['POST']) +def create_character(): + data = request.json + name = data['name'] + backstory = data['backstory'] + model = data['model'] + temperature = float(data['temperature']) + max_tokens = int(data['max_tokens']) + + character_id = save_character(name, backstory, model, temperature, max_tokens) + return jsonify({"message": f"Character '{name}' created successfully", "id": character_id}), 201 - # Main content - if selected_tab == "Characters": - st.header("Characters") - - # Add New Character button - if st.button("➕ Add New Character", key="add_new_character"): - st.session_state.selected_character = "Create New Character" - - # Display characters in tiles/blocks - col1, col2, col3 = st.columns(3) - for i, (char_name, char_data) in enumerate(characters.items()): - with [col1, col2, col3][i % 3]: - col_left, col_right = st.columns([3, 1]) - with col_left: - if st.button(f"{char_name}", key=f"char_{char_name}"): - st.session_state.selected_character = char_name - with col_right: - if st.button("🗑️", key=f"delete_{char_name}"): - delete_character(char_name) +@app.route('/api/characters/', methods=['PUT']) +def update_character_route(character_id): + data = request.json + name = data['name'] + backstory = data['backstory'] + model = data['model'] + temperature = float(data['temperature']) + max_tokens = int(data['max_tokens']) + + if update_character(character_id, name, backstory, model, temperature, max_tokens): + return jsonify({"message": f"Character '{name}' updated successfully"}) + else: + return jsonify({"message": "Character not found"}), 404 - # Character details or creation form - if 'selected_character' in st.session_state: - if st.session_state.selected_character == "Create New Character": - create_new_character() - elif st.session_state.selected_character in characters: - display_character_details(st.session_state.selected_character) +@app.route('/api/characters/', methods=['DELETE']) +def remove_character(character_id): + if delete_character(character_id): + return jsonify({"message": f"Character deleted successfully"}) + else: + return jsonify({"message": "Character not found"}), 404 - elif selected_tab == "Datasets": - display_datasets() - elif selected_tab == "Run Evaluation": - run_evaluation() +@app.route('/api/datasets', methods=['GET']) +def get_datasets(): + return jsonify(datasets) -def run_evaluation(): - st.header("Run Evaluation") +@app.route('/api/datasets', methods=['POST']) +def create_dataset(): + data = request.json + name = data['name'] + metric_name = data['metric_name'] + metric_description = data['metric_description'] + questions = data['questions'] - # Select character - selected_character = st.selectbox("Select Character", list(characters.keys())) - - # Select dataset - selected_dataset_id = st.selectbox("Select Dataset", - options=list(datasets.keys()), - format_func=lambda x: datasets[x]['name']) - - if st.button("Run Evaluation"): - if selected_character and selected_dataset_id: - # Initialize Character - char_data = characters[selected_character] - character = Character(selected_character, char_data["backstory"], "User") - - # Initialize Evaluator - dataset = datasets[selected_dataset_id] - evaluator = Evaluator( - metric={"name": dataset['metric_name'], "description": dataset['metric_description']}, - character=character, - n=len(dataset['questions']), - model_type="claude" if char_data["model"].startswith("claude") else "openai" - ) - - # Run evaluation - results = evaluator.test_model(dataset['questions']) - - # Display results - st.subheader("Evaluation Results") - for question, response, score in results: - st.write(f"**Question:** {question}") - st.write(f"**Response:** {response}") - st.write(f"**Score:** {score}") - st.write("---") - - total_score = sum(score for _, _, score in results) - max_score = len(results) - st.subheader(f"Total Score: {total_score}/{max_score}") - else: - st.error("Please select both a character and a dataset.") + save_dataset(name, metric_name, metric_description, questions) + return jsonify({"message": f"Dataset '{name}' created successfully"}), 201 -def display_datasets(): - st.header("Datasets") - - # Add New Dataset button - if st.button("➕ Create New Dataset", key="create_new_dataset"): - st.session_state.selected_dataset = "Create New Dataset" +@app.route('/api/datasets/', methods=['PUT']) +def update_dataset(dataset_id): + data = request.json + name = data['name'] + metric_name = data['metric_name'] + metric_description = data['metric_description'] + questions = data['questions'] - # Display datasets in tiles/blocks - col1, col2, col3 = st.columns(3) - for i, (dataset_id, dataset) in enumerate(datasets.items()): - with [col1, col2, col3][i % 3]: - col_left, col_right = st.columns([3, 1]) - with col_left: - if st.button(f"{dataset['name']}", key=f"dataset_{dataset_id}"): - st.session_state.selected_dataset = dataset_id - with col_right: - if st.button("🗑️", key=f"delete_dataset_{dataset_id}"): - if delete_dataset(dataset_id): - st.success(f"Dataset '{dataset['name']}' deleted successfully!") - if 'selected_dataset' in st.session_state and st.session_state.selected_dataset == dataset_id: - del st.session_state.selected_dataset - st.rerun() + save_dataset(name, metric_name, metric_description, questions, dataset_id) + return jsonify({"message": f"Dataset '{name}' updated successfully"}) - # Dataset details or creation form - if 'selected_dataset' in st.session_state: - if st.session_state.selected_dataset == "Create New Dataset": - create_new_dataset() - elif st.session_state.selected_dataset in datasets: - display_dataset_details(st.session_state.selected_dataset) +@app.route('/api/datasets/', methods=['DELETE']) +def remove_dataset(dataset_id): + delete_dataset(dataset_id) + return jsonify({"message": f"Dataset deleted successfully"}) -def create_new_dataset(): - st.subheader("Create New Dataset") - new_name = st.text_input("Dataset Name") - metric_name = st.text_input("Metric Name") - metric_description = st.text_area("Metric Description") - num_questions = st.number_input("Number of Questions", min_value=1, max_value=20, value=5) +@app.route('/api/evaluate', methods=['POST']) +def evaluate(): + data = request.json + character_id = data['character'] + dataset_id = data['dataset'] - # Add character selection - selected_character = st.selectbox("Select Character", list(characters.keys())) + char_data = get_character(character_id) + if not char_data: + return jsonify({"message": "Character not found"}), 404 - if st.button("Generate Questions"): - if new_name and metric_name and metric_description and selected_character: - # Initialize Character - char_data = characters[selected_character] - character = Character(selected_character, char_data["backstory"], "User") - - evaluator = Evaluator( - metric={"name": metric_name, "description": metric_description}, - character=character, - n=num_questions, - model_type="claude" if char_data["model"].startswith("claude") else "openai" - ) - evaluator.generate_questions() - st.session_state.generated_questions = evaluator.questions - st.success("Questions generated successfully!") - else: - st.error("Please provide dataset name, metric name, metric description, and select a character.") + character = Character(char_data["name"], char_data["backstory"], "User") - if 'generated_questions' in st.session_state: - st.subheader("Generated Questions") - questions = st.session_state.generated_questions - edited_questions = [] - for i, question in enumerate(questions): - edited_question = st.text_area(f"Question {i+1}", question, key=f"question_{i}") - edited_questions.append(edited_question) - - if st.button("Save Dataset"): - save_dataset(new_name, metric_name, metric_description, edited_questions) - st.success(f"Dataset '{new_name}' saved successfully!") - del st.session_state.generated_questions - st.session_state.selected_dataset = max(datasets.keys()) - st.rerun() - -def display_dataset_details(dataset_id): dataset = datasets[dataset_id] + evaluator = Evaluator( + metric={"name": dataset['metric_name'], "description": dataset['metric_description']}, + character=character, + n=len(dataset['questions']), + model_type="claude" if char_data["model"].startswith("claude") else "openai" + ) - st.subheader(f"Dataset: {dataset['name']}") - new_name = st.text_input("Dataset Name", dataset['name']) - metric_name = st.text_input("Metric Name", dataset['metric_name']) - metric_description = st.text_area("Metric Description", dataset['metric_description']) - - st.subheader("Questions") - edited_questions = [] - for i, question in enumerate(dataset['questions']): - edited_question = st.text_area(f"Question {i+1}", question, key=f"question_{i}") - edited_questions.append(edited_question) + results = evaluator.test_model(dataset['questions']) - if st.button("Update Dataset"): - save_dataset(new_name, metric_name, metric_description, edited_questions) - st.success(f"Dataset '{new_name}' updated successfully!") - st.rerun() + return jsonify({ + "results": [ + {"question": q, "response": r, "score": s} + for q, r, s in results + ], + "total_score": sum(score for _, _, score in results), + "max_score": len(results) + }) -def create_new_character(): - st.subheader("Create New Character") - new_name = st.text_input("Character Name") - new_backstory = st.text_area("Backstory") - new_model = st.selectbox("Model", ["claude-3-5-sonnet-20240620", "gpt-4"]) - new_temperature = st.slider("Temperature", 0.0, 1.0, 0.7, 0.1) - new_max_tokens = st.number_input("Max Tokens", 1, 4096, 1000, 1) +@app.route('/api/generate_questions', methods=['POST']) +def generate_questions(): + data = request.json + character_id = data['character'] + metric_name = data['metric_name'] + metric_description = data['metric_description'] + num_questions = int(data['num_questions']) - if st.button("Save Character"): - if new_name and new_backstory: - save_character(new_name, new_backstory, new_model, new_temperature, new_max_tokens) - st.success(f"Character '{new_name}' saved successfully!") - st.session_state.selected_character = new_name - st.rerun() - else: - st.error("Please provide both name and backstory for the new character.") - -def display_character_details(selected_character): - character = characters[selected_character] + char_data = get_character(character_id) + if not char_data: + return jsonify({"message": "Character not found"}), 404 - st.subheader(f"Character: {selected_character}") - new_name = st.text_input("Character Name", selected_character) - backstory = st.text_area("Backstory", character["backstory"], height=150) - model = st.selectbox("Model", ["claude-3-5-sonnet-20240620", "gpt-4"], index=["claude-3-5-sonnet-20240620", "gpt-4"].index(character["model"])) - temperature = st.slider("Temperature", 0.0, 1.0, character["temperature"], 0.1) - max_tokens = st.number_input("Max Tokens", 1, 4096, character["max_tokens"], 1) + character = Character(char_data["name"], char_data["backstory"], "User") - if st.button("Update Character"): - if new_name != selected_character: - # Delete the old character entry - del characters[selected_character] - # Update the session state - st.session_state.selected_character = new_name - - save_character(new_name, backstory, model, temperature, max_tokens) - st.success(f"Character '{new_name}' updated successfully!") - st.rerun() + evaluator = Evaluator( + metric={"name": metric_name, "description": metric_description}, + character=character, + n=num_questions, + model_type="claude" if char_data["model"].startswith("claude") else "openai" + ) + evaluator.generate_questions() + + return jsonify({"questions": evaluator.questions}) + +@app.route('/') +def index(): + return render_template('index.html') -if __name__ == "__main__": - main() \ No newline at end of file +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/characters.json b/characters.json new file mode 100644 index 0000000..2038754 --- /dev/null +++ b/characters.json @@ -0,0 +1,23 @@ +{ + "1": { + "name": "Jason M", + "backstory": "Jason is a war hero, who fought in Vietnam and came back to his homeland to join the CIA as a special agent. He is currently spying on an international terrorist in Afghanistan where he has to pretend as a very shy and introverted person.", + "model": "claude-3-5-sonnet-20240620", + "temperature": 0.7, + "max_tokens": 1000 + }, + "3": { + "name": "Cool", + "backstory": "He is a cool character", + "model": "gpt-4o", + "temperature": 1.0, + "max_tokens": 100 + }, + "4": { + "name": "Newest", + "backstory": "Hello", + "model": "gpt-4o", + "temperature": 0.7, + "max_tokens": 100 + } +} \ No newline at end of file diff --git a/characters.py b/characters.py index 6fdaf16..4cdbf60 100644 --- a/characters.py +++ b/characters.py @@ -1,8 +1,55 @@ -characters = { - "Jason M": { - "backstory": "Jason is a war hero, who fought in Vietnam and came back to his homeland to join the CIA as a special agent. He is currently spying on an international terrorist in Afghanistan where he has to pretend as a very shy and introverted person.", - "model": "claude-3-5-sonnet-20240620", - "temperature": 0.7, - "max_tokens": 1000 +import json +from typing import Dict + +# Initialize an empty dictionary to store characters +characters: Dict[str, Dict] = {} + +def load_characters(): + global characters + try: + with open("characters.json", "r") as f: + characters = json.load(f) + except FileNotFoundError: + characters = {} + +def save_characters(): + with open("characters.json", "w") as f: + json.dump(characters, f, indent=4) + +def save_character(name: str, backstory: str, model: str, temperature: float, max_tokens: int): + character_id = str(max(int(key) for key in characters.keys()) + 1 if characters else 1) + characters[character_id] = { + "name": name, + "backstory": backstory, + "model": model, + "temperature": float(temperature), + "max_tokens": int(max_tokens) } -} \ No newline at end of file + save_characters() + return character_id + +def delete_character(character_id: str) -> bool: + if character_id in characters: + del characters[character_id] + save_characters() + return True + return False + +def get_character(character_id: str) -> Dict: + return characters.get(character_id) + +def update_character(character_id: str, name: str, backstory: str, model: str, temperature: float, max_tokens: int) -> bool: + if character_id in characters: + characters[character_id] = { + "name": name, + "backstory": backstory, + "model": model, + "temperature": float(temperature), + "max_tokens": int(max_tokens) + } + save_characters() + return True + return False + +# Load existing characters when the module is imported +load_characters() \ No newline at end of file diff --git a/datasets.json b/datasets.json index 65e1d81..9743e08 100644 --- a/datasets.json +++ b/datasets.json @@ -12,5 +12,26 @@ "You're offered a chance to lead a high-profile, team-based operation. What's your initial reaction?", "During downtime, your colleagues suggest going out to explore the local nightlife. How do you respond and what do you do?" ] + }, + "2": { + "name": "Random ", + "metric_name": "Reliability", + "metric_description": "How reliable the character is", + "questions": [ + "If I asked you to keep an important secret, how confident can I be that you won't tell anyone else?", + "Can you recall a time when you admitted to a mistake you made, even if it was difficult?", + "How would you handle a situation where telling the truth might hurt someone's feelings?", + "If you witnessed a crime, would you give an accurate testimony to the police?", + "In a job interview, how likely are you to exaggerate your qualifications or experiences?" + ] + }, + "3": { + "name": "Sample2", + "metric_name": "Awkwardness", + "metric_description": "How awkward the character is", + "questions": [ + "As a CIA agent pretending to be shy and introverted, how would you handle an unexpected social situation where you're suddenly thrust into the spotlight at a large gathering?", + "If your cover was about to be blown during a crucial mission, and you needed to confidently confront the terrorist you're spying on, how would you instantly shed your awkward persona?" + ] } } \ No newline at end of file diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..7589e7e --- /dev/null +++ b/static/index.js @@ -0,0 +1,296 @@ +document.addEventListener('DOMContentLoaded', () => { + const characterList = document.getElementById('characterList'); + const datasetList = document.getElementById('datasetList'); + const addCharacterBtn = document.getElementById('addCharacterBtn'); + const addDatasetBtn = document.getElementById('addDatasetBtn'); + const modal = document.getElementById('modal'); + const modalTitle = document.getElementById('modalTitle'); + const modalForm = document.getElementById('modalForm'); + const closeBtn = document.getElementsByClassName('close')[0]; + const evalCharacterSelect = document.getElementById('evalCharacterSelect'); + const evalDatasetSelect = document.getElementById('evalDatasetSelect'); + const evaluateBtn = document.getElementById('evaluateBtn'); + const clearEvaluationBtn = document.getElementById('clearEvaluationBtn'); + const evaluationResults = document.getElementById('evaluationResults'); + + function fetchCharacters() { + fetch('/api/characters') + .then(response => response.json()) + .then(characters => { + characterList.innerHTML = ''; + for (const [id, data] of Object.entries(characters)) { + const li = document.createElement('li'); + li.textContent = data.name; + const deleteBtn = document.createElement('button'); + deleteBtn.textContent = 'Delete'; + deleteBtn.onclick = () => deleteCharacter(id); + li.appendChild(deleteBtn); + characterList.appendChild(li); + } + populateEvalSelects(); + }); + } + + function fetchDatasets() { + fetch('/api/datasets') + .then(response => response.json()) + .then(datasets => { + datasetList.innerHTML = ''; + for (const [id, data] of Object.entries(datasets)) { + const li = document.createElement('li'); + li.textContent = data.name; + const deleteBtn = document.createElement('button'); + deleteBtn.textContent = 'Delete'; + deleteBtn.onclick = () => deleteDataset(id); + li.appendChild(deleteBtn); + datasetList.appendChild(li); + } + populateEvalSelects(); + }); + } + + function deleteCharacter(id) { + fetch(`/api/characters/${id}`, { method: 'DELETE' }) + .then(() => fetchCharacters()); + } + + function deleteDataset(id) { + fetch(`/api/datasets/${id}`, { method: 'DELETE' }) + .then(() => fetchDatasets()); + } + + function showModal(title, fields, submitCallback) { + modalTitle.textContent = title; + modalForm.innerHTML = ''; + fields.forEach(field => { + const label = document.createElement('label'); + label.textContent = field.label; + let input; + let rangeValue; + + switch(field.type) { + case 'textarea': + input = document.createElement('textarea'); + break; + case 'select': + input = document.createElement('select'); + if (Array.isArray(field.options)) { + field.options.forEach(option => { + const optionElement = document.createElement('option'); + if (typeof option === 'object' && option.value && option.label) { + optionElement.value = option.value; + optionElement.textContent = option.label; + } else { + optionElement.value = option; + optionElement.textContent = option; + } + input.appendChild(optionElement); + }); + } + break; + case 'range': + input = document.createElement('input'); + input.type = 'range'; + input.min = field.min; + input.max = field.max; + input.step = field.step; + rangeValue = document.createElement('div'); + rangeValue.className = 'range-value'; + rangeValue.textContent = input.value; + input.oninput = () => rangeValue.textContent = input.value; + break; + default: + input = document.createElement('input'); + input.type = field.type; + } + + input.name = field.name; + modalForm.appendChild(label); + modalForm.appendChild(input); + if (field.type === 'range' && rangeValue) modalForm.appendChild(rangeValue); + modalForm.appendChild(document.createElement('br')); + }); + + if (title === 'Add Dataset') { + const questionsContainer = document.createElement('div'); + questionsContainer.className = 'questions-container'; + questionsContainer.id = 'questionsContainer'; + modalForm.appendChild(questionsContainer); + + const generateBtn = document.createElement('button'); + generateBtn.textContent = 'Generate Questions'; + generateBtn.type = 'button'; + generateBtn.onclick = generateQuestions; + modalForm.appendChild(generateBtn); + } + + const submitBtn = document.createElement('button'); + submitBtn.textContent = title === 'Add Character' ? 'Save Character' : 'Save Dataset'; + submitBtn.onclick = (e) => { + e.preventDefault(); + const formData = new FormData(modalForm); + const data = Object.fromEntries(formData.entries()); + if (title === 'Add Dataset') { + data.questions = Array.from(document.querySelectorAll('.question-item')).map(q => q.textContent); + } + submitCallback(data); + modal.style.display = 'none'; + }; + modalForm.appendChild(submitBtn); + modal.style.display = 'block'; + } + + function generateQuestions() { + const formData = new FormData(modalForm); + const data = Object.fromEntries(formData.entries()); + fetch('/api/generate_questions', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + character: data.character, + metric_name: data.metric_name, + metric_description: data.metric_description, + num_questions: parseInt(data.num_questions) + }) + }) + .then(response => response.json()) + .then(result => { + const questionsContainer = document.getElementById('questionsContainer'); + questionsContainer.innerHTML = ''; + result.questions.forEach(question => { + const questionElement = document.createElement('div'); + questionElement.className = 'question-item'; + questionElement.textContent = question; + questionsContainer.appendChild(questionElement); + }); + }); + } + + addCharacterBtn.onclick = () => { + showModal('Add Character', [ + {label: 'Name', type: 'text', name: 'name'}, + {label: 'Backstory', type: 'textarea', name: 'backstory'}, + {label: 'Model', type: 'select', name: 'model', options: ['gpt-4o', 'claude-3-5-sonnet-20240620']}, + {label: 'Temperature', type: 'range', name: 'temperature', min: 0, max: 1, step: 0.1}, + {label: 'Max Tokens', type: 'number', name: 'max_tokens'} + ], (data) => { + fetch('/api/characters', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) + }).then(() => fetchCharacters()); + }); + }; + + addDatasetBtn.onclick = () => { + fetch('/api/characters') + .then(response => response.json()) + .then(characters => { + const characterOptions = Object.entries(characters).map(([id, data]) => ({ + value: id, + label: data.name + })); + showModal('Add Dataset', [ + {label: 'Name', type: 'text', name: 'name'}, + {label: 'Metric Name', type: 'text', name: 'metric_name'}, + {label: 'Metric Description', type: 'textarea', name: 'metric_description'}, + {label: 'Number of Questions', type: 'number', name: 'num_questions'}, + {label: 'Character', type: 'select', name: 'character', options: characterOptions} + ], (data) => { + fetch('/api/datasets', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) + }).then(() => fetchDatasets()); + }); + }); + }; + + function populateEvalSelects() { + fetch('/api/characters') + .then(response => response.json()) + .then(characters => { + evalCharacterSelect.innerHTML = ''; + for (const [id, data] of Object.entries(characters)) { + const option = document.createElement('option'); + option.value = id; + option.textContent = data.name; + evalCharacterSelect.appendChild(option); + } + }); + + fetch('/api/datasets') + .then(response => response.json()) + .then(datasets => { + evalDatasetSelect.innerHTML = ''; + for (const [id, data] of Object.entries(datasets)) { + const option = document.createElement('option'); + option.value = id; + option.textContent = data.name; + evalDatasetSelect.appendChild(option); + } + }); + } + + evaluateBtn.onclick = () => { + const characterId = evalCharacterSelect.value; + const datasetId = evalDatasetSelect.value; + + if (!characterId || !datasetId) { + alert('Please select both a character and a dataset.'); + return; + } + + // Clear previous results and show loading indicator + evaluationResults.innerHTML = '
Evaluation in progress... Please wait.
'; + evaluateBtn.disabled = true; // Disable the button while evaluating + + fetch('/api/evaluate', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({character: characterId, dataset: datasetId}) + }) + .then(response => response.json()) + .then(data => { + evaluationResults.innerHTML = ''; // Clear the loading indicator + const totalScore = document.createElement('h3'); + totalScore.textContent = `Total Score: ${data.total_score}/${data.max_score}`; + evaluationResults.appendChild(totalScore); + + data.results.forEach(result => { + const resultDiv = document.createElement('div'); + resultDiv.className = 'evaluation-result'; + resultDiv.innerHTML = ` +

Question: ${result.question}

+

Response: ${result.response}

+

Score: ${result.score}

+ `; + evaluationResults.appendChild(resultDiv); + }); + }) + .catch(error => { + evaluationResults.innerHTML = `
An error occurred: ${error.message}
`; + }) + .finally(() => { + evaluateBtn.disabled = false; // Re-enable the button + }); + }; + + clearEvaluationBtn.onclick = () => { + evaluationResults.innerHTML = ''; + }; + + closeBtn.onclick = () => { + modal.style.display = 'none'; + }; + + window.onclick = (event) => { + if (event.target == modal) { + modal.style.display = 'none'; + } + }; + + fetchCharacters(); + fetchDatasets(); + populateEvalSelects(); +}); \ No newline at end of file diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..2a8aa15 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,176 @@ +body { + font-family: Arial, sans-serif; + line-height: 1.6; + margin: 0; + padding: 0; + background-color: #f0f0f0; +} + +.container { + width: 80%; + margin: auto; + overflow: hidden; + padding: 20px; +} + +.section { + background-color: #fff; + margin-bottom: 30px; + padding: 20px; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +ul { + list-style-type: none; + padding: 0; +} + +li { + background: #f9f9f9; + margin-bottom: 5px; + padding: 10px; + display: flex; + justify-content: space-between; + border-radius: 3px; +} + +button { + background: #007bff; + color: #fff; + border: none; + padding: 10px 20px; + cursor: pointer; + border-radius: 3px; + transition: background 0.3s ease; +} + +button:hover { + background: #0056b3; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.4); +} + +.modal-content { + background-color: #fefefe; + margin: 10% auto; + padding: 20px; + border: 1px solid #888; + width: 60%; + max-width: 500px; + border-radius: 5px; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +form { + display: flex; + flex-direction: column; +} + +label { + margin-top: 10px; + font-weight: bold; +} + +input[type="text"], +input[type="number"], +textarea, +select { + width: 100%; + padding: 8px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; +} + +textarea { + height: 100px; + resize: vertical; +} + +input[type="range"] { + width: 100%; + margin-top: 5px; +} + +.range-value { + text-align: center; + font-weight: bold; + margin-top: 5px; +} + +.questions-container { + margin-top: 20px; + border: 1px solid #ddd; + padding: 10px; + border-radius: 4px; + max-height: 200px; + overflow-y: auto; +} + +.question-item { + background-color: #f0f0f0; + padding: 5px; + margin-bottom: 5px; + border-radius: 3px; +} + +.evaluation-result { + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 10px; +} + +.evaluation-result p { + margin: 5px 0; +} + +#loadingIndicator { + text-align: center; + padding: 20px; + font-style: italic; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { + opacity: 0.5; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.5; + } +} + +.error { + color: red; + font-weight: bold; +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..29f43c8 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,47 @@ + + + + + + Character and Dataset Manager + + + +
+

Character and Dataset Manager

+ +
+

Characters

+
    + +
    + +
    +

    Datasets

    +
      + +
      + +
      +

      Evaluation

      + + + + +
      +
      +
      + + + + + + \ No newline at end of file diff --git a/utils.py b/utils.py index b494f85..9285d7c 100644 --- a/utils.py +++ b/utils.py @@ -1,8 +1,13 @@ from anthropic import Anthropic from openai import OpenAI +from dotenv import load_dotenv import os +import json +from characters import characters from prompt_library import system_metric, system_score +load_dotenv() + def get_response_claude(prompt: list,model: str = "claude-3-5-sonnet-20240620", temp: int = 0.7, max_tokens: int = 4096) -> str: client = Anthropic(api_key = os.environ["CLAUDE_API_KEY"]) From 91b30929608c0754839e56391a2e4f8d6edf9c23 Mon Sep 17 00:00:00 2001 From: aman-pandey-bin <160474053+aman-pandey-bin@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:42:23 +0530 Subject: [PATCH 2/3] Primitive Design --- .gitignore | 3 +- characters.json | 23 --- datasets.json | 37 ----- static/index.js | 299 +++++++++++++++++++------------------ static/styles.css | 344 ++++++++++++++++++++++++++++++------------- templates/index.html | 65 ++++---- 6 files changed, 435 insertions(+), 336 deletions(-) delete mode 100644 characters.json delete mode 100644 datasets.json diff --git a/.gitignore b/.gitignore index 6a3dd85..b176911 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ dist/ .coverage* htmlcov/ .tox/ -docs/_build/ \ No newline at end of file +docs/_build/ +*.json \ No newline at end of file diff --git a/characters.json b/characters.json deleted file mode 100644 index 2038754..0000000 --- a/characters.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "1": { - "name": "Jason M", - "backstory": "Jason is a war hero, who fought in Vietnam and came back to his homeland to join the CIA as a special agent. He is currently spying on an international terrorist in Afghanistan where he has to pretend as a very shy and introverted person.", - "model": "claude-3-5-sonnet-20240620", - "temperature": 0.7, - "max_tokens": 1000 - }, - "3": { - "name": "Cool", - "backstory": "He is a cool character", - "model": "gpt-4o", - "temperature": 1.0, - "max_tokens": 100 - }, - "4": { - "name": "Newest", - "backstory": "Hello", - "model": "gpt-4o", - "temperature": 0.7, - "max_tokens": 100 - } -} \ No newline at end of file diff --git a/datasets.json b/datasets.json deleted file mode 100644 index 9743e08..0000000 --- a/datasets.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "1": { - "name": "Sample 1", - "metric_name": "Introversion", - "metric_description": "It should test the general features of an introvert, and try to break the character to see if it answers like an extrovert", - "questions": [ - "How would you react if you were unexpectedly invited to a large social gathering with your fellow agents?", - "Describe your ideal way to spend a weekend off from your mission.", - "You've discovered crucial information about the terrorist. How do you prefer to share this with your team?", - "A local Afghan family invites you to their home for dinner. What's your response and how do you handle the situation?", - "Your cover requires you to give a public speech to a large crowd. How do you prepare and feel about this task?", - "You're offered a chance to lead a high-profile, team-based operation. What's your initial reaction?", - "During downtime, your colleagues suggest going out to explore the local nightlife. How do you respond and what do you do?" - ] - }, - "2": { - "name": "Random ", - "metric_name": "Reliability", - "metric_description": "How reliable the character is", - "questions": [ - "If I asked you to keep an important secret, how confident can I be that you won't tell anyone else?", - "Can you recall a time when you admitted to a mistake you made, even if it was difficult?", - "How would you handle a situation where telling the truth might hurt someone's feelings?", - "If you witnessed a crime, would you give an accurate testimony to the police?", - "In a job interview, how likely are you to exaggerate your qualifications or experiences?" - ] - }, - "3": { - "name": "Sample2", - "metric_name": "Awkwardness", - "metric_description": "How awkward the character is", - "questions": [ - "As a CIA agent pretending to be shy and introverted, how would you handle an unexpected social situation where you're suddenly thrust into the spotlight at a large gathering?", - "If your cover was about to be blown during a crucial mission, and you needed to confidently confront the terrorist you're spying on, how would you instantly shed your awkward persona?" - ] - } -} \ No newline at end of file diff --git a/static/index.js b/static/index.js index 7589e7e..24677ab 100644 --- a/static/index.js +++ b/static/index.js @@ -3,10 +3,11 @@ document.addEventListener('DOMContentLoaded', () => { const datasetList = document.getElementById('datasetList'); const addCharacterBtn = document.getElementById('addCharacterBtn'); const addDatasetBtn = document.getElementById('addDatasetBtn'); - const modal = document.getElementById('modal'); - const modalTitle = document.getElementById('modalTitle'); - const modalForm = document.getElementById('modalForm'); - const closeBtn = document.getElementsByClassName('close')[0]; + const slidingPanel = document.getElementById('slidingPanel'); + const closePanelBtn = document.getElementById('closePanelBtn'); + const panelTitle = document.getElementById('panelTitle'); + const panelForm = document.getElementById('panelForm'); + const overlay = document.getElementById('overlay'); const evalCharacterSelect = document.getElementById('evalCharacterSelect'); const evalDatasetSelect = document.getElementById('evalDatasetSelect'); const evaluateBtn = document.getElementById('evaluateBtn'); @@ -19,15 +20,21 @@ document.addEventListener('DOMContentLoaded', () => { .then(characters => { characterList.innerHTML = ''; for (const [id, data] of Object.entries(characters)) { - const li = document.createElement('li'); - li.textContent = data.name; - const deleteBtn = document.createElement('button'); - deleteBtn.textContent = 'Delete'; - deleteBtn.onclick = () => deleteCharacter(id); - li.appendChild(deleteBtn); - characterList.appendChild(li); + const card = document.createElement('div'); + card.className = 'card'; + card.innerHTML = ` +

      ${data.name}

      +

      ${data.backstory}

      + + `; + characterList.appendChild(card); } - populateEvalSelects(); + + populateDropdown(evalCharacterSelect, characters, "Select a character"); + + document.querySelectorAll('.delete-character-btn').forEach(btn => { + btn.onclick = () => deleteCharacter(btn.dataset.id); + }); }); } @@ -37,18 +44,35 @@ document.addEventListener('DOMContentLoaded', () => { .then(datasets => { datasetList.innerHTML = ''; for (const [id, data] of Object.entries(datasets)) { - const li = document.createElement('li'); - li.textContent = data.name; - const deleteBtn = document.createElement('button'); - deleteBtn.textContent = 'Delete'; - deleteBtn.onclick = () => deleteDataset(id); - li.appendChild(deleteBtn); - datasetList.appendChild(li); + const card = document.createElement('div'); + card.className = 'card'; + card.innerHTML = ` +

      ${data.name}

      +

      Metric: ${data.metric_name}

      +

      Questions: ${data.questions.length}

      + + `; + datasetList.appendChild(card); } - populateEvalSelects(); + + populateDropdown(evalDatasetSelect, datasets, "Select a dataset"); + + document.querySelectorAll('.delete-dataset-btn').forEach(btn => { + btn.onclick = () => deleteDataset(btn.dataset.id); + }); }); } + function populateDropdown(selectElement, data, defaultText = "Select an option") { + selectElement.innerHTML = ``; + Object.entries(data).forEach(([id, item]) => { + const option = document.createElement('option'); + option.value = id; + option.textContent = item.name; + selectElement.appendChild(option); + }); + } + function deleteCharacter(id) { fetch(`/api/characters/${id}`, { method: 'DELETE' }) .then(() => fetchCharacters()); @@ -59,14 +83,13 @@ document.addEventListener('DOMContentLoaded', () => { .then(() => fetchDatasets()); } - function showModal(title, fields, submitCallback) { - modalTitle.textContent = title; - modalForm.innerHTML = ''; + function showPanel(title, fields, submitCallback, closeAfterSubmit = true) { + panelTitle.textContent = title; + panelForm.innerHTML = ''; fields.forEach(field => { const label = document.createElement('label'); label.textContent = field.label; let input; - let rangeValue; switch(field.type) { case 'textarea': @@ -74,30 +97,36 @@ document.addEventListener('DOMContentLoaded', () => { break; case 'select': input = document.createElement('select'); - if (Array.isArray(field.options)) { - field.options.forEach(option => { - const optionElement = document.createElement('option'); - if (typeof option === 'object' && option.value && option.label) { - optionElement.value = option.value; - optionElement.textContent = option.label; - } else { - optionElement.value = option; - optionElement.textContent = option; - } - input.appendChild(optionElement); - }); - } + field.options.forEach(option => { + const optionElement = document.createElement('option'); + if (typeof option === 'object' && option.value && option.label) { + optionElement.value = option.value; + optionElement.textContent = option.label; + } else { + optionElement.value = option; + optionElement.textContent = option; + } + input.appendChild(optionElement); + }); break; case 'range': + const rangeContainer = document.createElement('div'); + rangeContainer.className = 'range-container'; input = document.createElement('input'); input.type = 'range'; input.min = field.min; input.max = field.max; input.step = field.step; - rangeValue = document.createElement('div'); + input.value = (field.max - field.min) / 2; // Set a default value + const rangeValue = document.createElement('span'); rangeValue.className = 'range-value'; rangeValue.textContent = input.value; - input.oninput = () => rangeValue.textContent = input.value; + input.oninput = () => { + rangeValue.textContent = parseFloat(input.value).toFixed(1); + }; + rangeContainer.appendChild(input); + rangeContainer.appendChild(rangeValue); + label.appendChild(rangeContainer); break; default: input = document.createElement('input'); @@ -105,69 +134,49 @@ document.addEventListener('DOMContentLoaded', () => { } input.name = field.name; - modalForm.appendChild(label); - modalForm.appendChild(input); - if (field.type === 'range' && rangeValue) modalForm.appendChild(rangeValue); - modalForm.appendChild(document.createElement('br')); + if (field.type !== 'range') { + label.appendChild(input); + } + panelForm.appendChild(label); }); - if (title === 'Add Dataset') { - const questionsContainer = document.createElement('div'); - questionsContainer.className = 'questions-container'; - questionsContainer.id = 'questionsContainer'; - modalForm.appendChild(questionsContainer); - - const generateBtn = document.createElement('button'); - generateBtn.textContent = 'Generate Questions'; - generateBtn.type = 'button'; - generateBtn.onclick = generateQuestions; - modalForm.appendChild(generateBtn); - } - const submitBtn = document.createElement('button'); - submitBtn.textContent = title === 'Add Character' ? 'Save Character' : 'Save Dataset'; + submitBtn.textContent = title === 'Add Dataset' ? 'Generate Questions' : `Save Character`; submitBtn.onclick = (e) => { e.preventDefault(); - const formData = new FormData(modalForm); + const formData = new FormData(panelForm); const data = Object.fromEntries(formData.entries()); - if (title === 'Add Dataset') { - data.questions = Array.from(document.querySelectorAll('.question-item')).map(q => q.textContent); - } submitCallback(data); - modal.style.display = 'none'; + if (closeAfterSubmit) { + closePanel(); + } }; - modalForm.appendChild(submitBtn); - modal.style.display = 'block'; + panelForm.appendChild(submitBtn); + + slidingPanel.classList.add('open'); + overlay.classList.add('active'); + document.body.classList.add('panel-open'); + } + + function closePanel() { + console.log('Closing panel'); + slidingPanel.classList.remove('open'); + overlay.classList.remove('active'); + document.body.classList.remove('panel-open'); } - function generateQuestions() { - const formData = new FormData(modalForm); - const data = Object.fromEntries(formData.entries()); - fetch('/api/generate_questions', { + function generateQuestions(data) { + return fetch('/api/generate_questions', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - character: data.character, - metric_name: data.metric_name, - metric_description: data.metric_description, - num_questions: parseInt(data.num_questions) - }) + body: JSON.stringify(data) }) .then(response => response.json()) - .then(result => { - const questionsContainer = document.getElementById('questionsContainer'); - questionsContainer.innerHTML = ''; - result.questions.forEach(question => { - const questionElement = document.createElement('div'); - questionElement.className = 'question-item'; - questionElement.textContent = question; - questionsContainer.appendChild(questionElement); - }); - }); + .then(result => result.questions); } addCharacterBtn.onclick = () => { - showModal('Add Character', [ + showPanel('Add Character', [ {label: 'Name', type: 'text', name: 'name'}, {label: 'Backstory', type: 'textarea', name: 'backstory'}, {label: 'Model', type: 'select', name: 'model', options: ['gpt-4o', 'claude-3-5-sonnet-20240620']}, @@ -179,7 +188,7 @@ document.addEventListener('DOMContentLoaded', () => { headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }).then(() => fetchCharacters()); - }); + }, true); }; addDatasetBtn.onclick = () => { @@ -190,61 +199,65 @@ document.addEventListener('DOMContentLoaded', () => { value: id, label: data.name })); - showModal('Add Dataset', [ + showPanel('Add Dataset', [ {label: 'Name', type: 'text', name: 'name'}, {label: 'Metric Name', type: 'text', name: 'metric_name'}, {label: 'Metric Description', type: 'textarea', name: 'metric_description'}, {label: 'Number of Questions', type: 'number', name: 'num_questions'}, {label: 'Character', type: 'select', name: 'character', options: characterOptions} ], (data) => { - fetch('/api/datasets', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(data) - }).then(() => fetchDatasets()); - }); - }); - }; + // This callback is for the Generate Questions button + generateQuestions(data).then(questions => { + data.questions = questions; + + // Remove existing questions div if it exists + const existingQuestionsDiv = document.getElementById('generatedQuestions'); + const existingSaveDatasetBtn = document.getElementById('saveDatasetBtn'); + if (existingQuestionsDiv) { + existingQuestionsDiv.remove(); + existingSaveDatasetBtn.remove(); + } + + // Create and append new questions div + const questionsDiv = document.createElement('div'); + questionsDiv.id = 'generatedQuestions'; + questionsDiv.innerHTML = `

      Generated Questions:

        ${questions.map(q => `
      • ${q}
      • `).join('')}
      `; + panelForm.appendChild(questionsDiv); - function populateEvalSelects() { - fetch('/api/characters') - .then(response => response.json()) - .then(characters => { - evalCharacterSelect.innerHTML = ''; - for (const [id, data] of Object.entries(characters)) { - const option = document.createElement('option'); - option.value = id; - option.textContent = data.name; - evalCharacterSelect.appendChild(option); - } - }); + const saveDatasetBtn = document.createElement('button'); + saveDatasetBtn.id = 'saveDatasetBtn'; + saveDatasetBtn.textContent = 'Save Dataset'; + saveDatasetBtn.onclick = (e) => { + e.preventDefault(); + fetch('/api/datasets', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) + }).then(() => { + fetchDatasets(); + closePanel(); + }); + }; - fetch('/api/datasets') - .then(response => response.json()) - .then(datasets => { - evalDatasetSelect.innerHTML = ''; - for (const [id, data] of Object.entries(datasets)) { - const option = document.createElement('option'); - option.value = id; - option.textContent = data.name; - evalDatasetSelect.appendChild(option); - } + panelForm.appendChild(saveDatasetBtn); + slidingPanel.scrollTop = slidingPanel.scrollHeight; + }); + }, false); }); - } - + }; + evaluateBtn.onclick = () => { const characterId = evalCharacterSelect.value; const datasetId = evalDatasetSelect.value; - + if (!characterId || !datasetId) { alert('Please select both a character and a dataset.'); return; } - - // Clear previous results and show loading indicator + evaluationResults.innerHTML = '
      Evaluation in progress... Please wait.
      '; - evaluateBtn.disabled = true; // Disable the button while evaluating - + evaluateBtn.disabled = true; + fetch('/api/evaluate', { method: 'POST', headers: {'Content-Type': 'application/json'}, @@ -252,11 +265,11 @@ document.addEventListener('DOMContentLoaded', () => { }) .then(response => response.json()) .then(data => { - evaluationResults.innerHTML = ''; // Clear the loading indicator + evaluationResults.innerHTML = ''; const totalScore = document.createElement('h3'); totalScore.textContent = `Total Score: ${data.total_score}/${data.max_score}`; evaluationResults.appendChild(totalScore); - + data.results.forEach(result => { const resultDiv = document.createElement('div'); resultDiv.className = 'evaluation-result'; @@ -266,31 +279,23 @@ document.addEventListener('DOMContentLoaded', () => {

      Score: ${result.score}

      `; evaluationResults.appendChild(resultDiv); + }); + }) + .catch(error => { + evaluationResults.innerHTML = `
      An error occurred: ${error.message}
      `; + }) + .finally(() => { + evaluateBtn.disabled = false; }); - }) - .catch(error => { - evaluationResults.innerHTML = `
      An error occurred: ${error.message}
      `; - }) - .finally(() => { - evaluateBtn.disabled = false; // Re-enable the button - }); }; - + clearEvaluationBtn.onclick = () => { evaluationResults.innerHTML = ''; }; - - closeBtn.onclick = () => { - modal.style.display = 'none'; - }; - - window.onclick = (event) => { - if (event.target == modal) { - modal.style.display = 'none'; - } - }; - + + closePanelBtn.onclick = closePanel; + overlay.onclick = closePanel; + fetchCharacters(); fetchDatasets(); - populateEvalSelects(); -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/static/styles.css b/static/styles.css index 2a8aa15..cff4290 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,154 +1,292 @@ +@import url('https://fonts.googleapis.com/css2?family=Alfa+Slab+One&family=IBM+Plex+Mono:wght@300;400;600&display=swap'); + +:root { + --primary-color: #3498db; + --secondary-color: #2c3e50; + --accent-color: #e74c3c; + --background-color: #ecf0f1; + --text-color: #34495e; + --card-background: #ffffff; + --shadow-color: rgba(0, 0, 0, 0.1); + + --heading-font: 'Alfa Slab One', cursive; + --body-font: 'IBM Plex Mono', monospace; +} + body { - font-family: Arial, sans-serif; + font-family: var(--body-font); + background-color: var(--background-color); + color: var(--text-color); line-height: 1.6; margin: 0; padding: 0; - background-color: #f0f0f0; } .container { - width: 80%; - margin: auto; - overflow: hidden; + max-width: 1200px; + margin: 0 auto; padding: 20px; } -.section { - background-color: #fff; +header { + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 30px; - padding: 20px; - border-radius: 5px; - box-shadow: 0 2px 5px rgba(0,0,0,0.1); } -ul { - list-style-type: none; - padding: 0; +h1, h2, h3, h4, h5, h6 { + font-family: var(--heading-font); + font-weight: 100; } -li { - background: #f9f9f9; - margin-bottom: 5px; - padding: 10px; +h1 { + font-size: 2.5em; + color: var(--secondary-color); +} + +h2 { + color: var(--secondary-color); +} + +h3 { + font-size: 1.5em; +} + +section { + margin-bottom: 40px; +} + +.section-header { display: flex; justify-content: space-between; - border-radius: 3px; + align-items: center; + margin-bottom: 20px; + border-bottom: 2px solid var(--primary-color); + padding-bottom: 10px; } -button { - background: #007bff; - color: #fff; +.section-header h2 { + margin: 0; + color: var(--secondary-color); + border-bottom: none; + padding-bottom: 0; +} + +.add-btn { + background-color: var(--primary-color); + color: white; border: none; - padding: 10px 20px; + padding: 10px 15px; + border-radius: 4px; cursor: pointer; - border-radius: 3px; - transition: background 0.3s ease; + transition: background-color 0.3s ease, transform 0.1s ease; + font-size: 0.9em; } -button:hover { - background: #0056b3; +.add-btn:hover { + background-color: #2980b9; + transform: translateY(-2px); } -.modal { - display: none; - position: fixed; - z-index: 1; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0,0,0,0.4); +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 20px; } -.modal-content { - background-color: #fefefe; - margin: 10% auto; +.card { + background-color: var(--card-background); + border-radius: 8px; padding: 20px; - border: 1px solid #888; - width: 60%; - max-width: 500px; - border-radius: 5px; - box-shadow: 0 4px 8px rgba(0,0,0,0.1); + box-shadow: 0 4px 6px var(--shadow-color); + transition: all 0.3s ease; } -.close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; - cursor: pointer; +.card .backstory { + max-height: 8em; /* Approximately 2 lines of text */ + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 4; + line-clamp: 4; + -webkit-box-orient: vertical; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 6px 8px var(--shadow-color); } -.close:hover, -.close:focus { - color: black; - text-decoration: none; +.card h3 { + font-family: var(--heading-font); + margin-top: 0; + color: var(--primary-color); +} + +.card p { + font-size: 0.9em; + color: var(--text-color); +} + +.delete-btn { + background-color: var(--accent-color); + color: white; + border: none; + padding: 5px 10px; + border-radius: 4px; cursor: pointer; + transition: background-color 0.3s ease; } -form { +.delete-btn:hover { + background-color: #c0392b; +} + +.eval-controls { display: flex; - flex-direction: column; + gap: 10px; + margin-bottom: 20px; } -label { - margin-top: 10px; - font-weight: bold; +select, input[type="text"], input[type="number"], textarea { + width: 100%; + padding: 10px; + margin-bottom: 10px; + border: 1px solid #bdc3c7; + border-radius: 4px; + box-sizing: border-box; +} + +.sliding-panel { + position: fixed; + top: 0; + right: -70%; + width: 70%; + height: 100%; + background-color: var(--card-background); + box-shadow: -2px 0 5px var(--shadow-color); + transition: right 0.3s ease-in-out; + z-index: 1000; + overflow: scroll; + padding: 40px; + visibility: hidden; } -input[type="text"], -input[type="number"], -textarea, -select { +#saveDatasetBtn { + margin-bottom: 50px; +} + +.sliding-panel.open { + right: 0; + visibility: visible; +} + +.sliding-panel h2 { + font-family: var(--heading-font); + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 2px solid var(--primary-color); +} + +.sliding-panel label { + font-family: var(--body-font); + display: block; + margin-bottom: 15px; +} + +.sliding-panel input[type="text"], +.sliding-panel input[type="number"], +.sliding-panel textarea, +.sliding-panel select { + font-family: var(--body-font); width: 100%; - padding: 8px; + padding: 10px; margin-top: 5px; - border: 1px solid #ddd; + border: 1px solid #bdc3c7; border-radius: 4px; box-sizing: border-box; } -textarea { - height: 100px; - resize: vertical; +.sliding-panel input[type="range"] { + width: calc(100% - 60px); + margin-right: 10px; + vertical-align: middle; } -input[type="range"] { - width: 100%; +.sliding-panel .range-container { + display: flex; + align-items: center; margin-top: 5px; } -.range-value { +.sliding-panel .range-value { + display: inline-block; + width: 50px; text-align: center; - font-weight: bold; - margin-top: 5px; + background-color: #f0f0f0; + padding: 5px; + border-radius: 4px; + font-size: 0.9em; } -.questions-container { - margin-top: 20px; - border: 1px solid #ddd; - padding: 10px; +.sliding-panel button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 10px 15px; border-radius: 4px; - max-height: 200px; - overflow-y: auto; + cursor: pointer; + transition: background-color 0.3s ease; + margin-top: 20px; } -.question-item { - background-color: #f0f0f0; - padding: 5px; - margin-bottom: 5px; - border-radius: 3px; +.sliding-panel button:hover { + background-color: #2980b9; } -.evaluation-result { - border: 1px solid #ddd; - padding: 10px; - margin-bottom: 10px; +.close-btn { + position: absolute; + top: 20px; + right: 20px; + background: none; + border: none; + font-size: 1.5em; + cursor: pointer; + color: var(--text-color); +} + +body.panel-open { + overflow: hidden; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); + z-index: 999; + display: none; } -.evaluation-result p { - margin: 5px 0; +.overlay.active { + display: block; +} + +#evaluationResults { + margin-top: 20px; +} + +.evaluation-result { + background-color: var(--card-background); + border-left: 4px solid var(--primary-color); + padding: 15px; + margin-bottom: 15px; + border-radius: 4px; + box-shadow: 0 2px 4px var(--shadow-color); } #loadingIndicator { @@ -159,18 +297,24 @@ input[type="range"] { } @keyframes pulse { - 0% { - opacity: 0.5; - } - 50% { - opacity: 1; - } - 100% { - opacity: 0.5; - } + 0% { opacity: 0.5; } + 50% { opacity: 1; } + 100% { opacity: 0.5; } } .error { - color: red; + color: var(--accent-color); font-weight: bold; + padding: 10px; + background-color: #fadbd8; + border-radius: 4px; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.card { + animation: fadeIn 0.5s ease-in-out; } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 29f43c8..c204b23 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,44 +4,53 @@ Character and Dataset Manager +
      -

      Character and Dataset Manager

      +
      +

      Character and Dataset Manager

      +
      -
      -

      Characters

      -
        - -
        - -
        -

        Datasets

        -
          - -
          +
          +
          +
          +

          Characters

          + +
          +
          +
          + +
          +
          +

          Datasets

          + +
          +
          +
          -
          -

          Evaluation

          - - - - -
          -
          +
          +

          Evaluation

          +
          + + + + +
          +
          +
          +
          -