feat(assistant): add multi-dialog UX, task-aware llm settings, and i18n cleanup
This commit is contained in:
@@ -218,6 +218,29 @@ def test_unknown_command_returns_needs_clarification():
|
||||
|
||||
|
||||
# [/DEF:test_unknown_command_returns_needs_clarification:Function]
|
||||
|
||||
|
||||
# [DEF:test_capabilities_question_returns_successful_help:Function]
|
||||
# @PURPOSE: Capability query should return deterministic help response, not clarification.
|
||||
# @PRE: User sends natural-language "what can you do" style query.
|
||||
# @POST: Response is successful and includes capabilities summary.
|
||||
def test_capabilities_question_returns_successful_help():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(message="Что ты умеешь?"),
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert response.state == "success"
|
||||
assert "Вот что я могу сделать" in response.text
|
||||
assert "Миграции" in response.text or "Git" in response.text
|
||||
|
||||
|
||||
# [/DEF:test_capabilities_question_returns_successful_help:Function]
|
||||
# [DEF:test_non_admin_command_returns_denied:Function]
|
||||
# @PURPOSE: Non-admin user must receive denied state for privileged command.
|
||||
# @PRE: Limited principal executes privileged git branch command.
|
||||
|
||||
@@ -27,6 +27,11 @@ from ...core.config_manager import ConfigManager
|
||||
from ...core.database import get_db
|
||||
from ...services.git_service import GitService
|
||||
from ...services.llm_provider import LLMProviderService
|
||||
from ...services.llm_prompt_templates import (
|
||||
is_multimodal_model,
|
||||
normalize_llm_settings,
|
||||
resolve_bound_provider_id,
|
||||
)
|
||||
from ...core.superset_client import SupersetClient
|
||||
from ...plugins.llm_analysis.service import LLMClient
|
||||
from ...plugins.llm_analysis.models import LLMProviderType
|
||||
@@ -449,7 +454,12 @@ def _is_production_env(token: Optional[str], config_manager: ConfigManager) -> b
|
||||
# @PURPOSE: Resolve provider token to provider id with active/default fallback.
|
||||
# @PRE: db session can load provider list through LLMProviderService.
|
||||
# @POST: Returns provider id or None when no providers configured.
|
||||
def _resolve_provider_id(provider_token: Optional[str], db: Session) -> Optional[str]:
|
||||
def _resolve_provider_id(
|
||||
provider_token: Optional[str],
|
||||
db: Session,
|
||||
config_manager: Optional[ConfigManager] = None,
|
||||
task_key: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
service = LLMProviderService(db)
|
||||
providers = service.get_all_providers()
|
||||
if not providers:
|
||||
@@ -461,6 +471,15 @@ def _resolve_provider_id(provider_token: Optional[str], db: Session) -> Optional
|
||||
if p.id.lower() == needle or p.name.lower() == needle:
|
||||
return p.id
|
||||
|
||||
if config_manager and task_key:
|
||||
try:
|
||||
llm_settings = config_manager.get_config().settings.llm
|
||||
bound_provider_id = resolve_bound_provider_id(llm_settings, task_key)
|
||||
if bound_provider_id and any(p.id == bound_provider_id for p in providers):
|
||||
return bound_provider_id
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
active = next((p for p in providers if p.is_active), None)
|
||||
return active.id if active else providers[0].id
|
||||
# [/DEF:_resolve_provider_id:Function]
|
||||
@@ -537,6 +556,27 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any
|
||||
text = message.strip()
|
||||
lower = text.lower()
|
||||
|
||||
if any(
|
||||
phrase in lower
|
||||
for phrase in [
|
||||
"что ты умеешь",
|
||||
"что умеешь",
|
||||
"что ты можешь",
|
||||
"help",
|
||||
"помощь",
|
||||
"доступные команды",
|
||||
"какие команды",
|
||||
]
|
||||
):
|
||||
return {
|
||||
"domain": "assistant",
|
||||
"operation": "show_capabilities",
|
||||
"entities": {},
|
||||
"confidence": 0.98,
|
||||
"risk_level": "safe",
|
||||
"requires_confirmation": False,
|
||||
}
|
||||
|
||||
dashboard_id = _extract_id(lower, [r"(?:дашборд\w*|dashboard)\s*(?:id\s*)?(\d+)"])
|
||||
dashboard_ref = _extract_id(
|
||||
lower,
|
||||
@@ -721,10 +761,26 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S
|
||||
envs = config_manager.get_environments()
|
||||
default_env_id = _get_default_environment_id(config_manager)
|
||||
providers = LLMProviderService(db).get_all_providers()
|
||||
llm_settings = {}
|
||||
try:
|
||||
llm_settings = config_manager.get_config().settings.llm
|
||||
except Exception:
|
||||
llm_settings = {}
|
||||
active_provider = next((p.id for p in providers if p.is_active), None)
|
||||
fallback_provider = active_provider or (providers[0].id if providers else None)
|
||||
validation_provider = resolve_bound_provider_id(llm_settings, "dashboard_validation") or fallback_provider
|
||||
documentation_provider = resolve_bound_provider_id(llm_settings, "documentation") or fallback_provider
|
||||
|
||||
candidates: List[Dict[str, Any]] = [
|
||||
{
|
||||
"operation": "show_capabilities",
|
||||
"domain": "assistant",
|
||||
"description": "Show available assistant commands and examples",
|
||||
"required_entities": [],
|
||||
"optional_entities": [],
|
||||
"risk_level": "safe",
|
||||
"requires_confirmation": False,
|
||||
},
|
||||
{
|
||||
"operation": "get_task_status",
|
||||
"domain": "status",
|
||||
@@ -785,7 +841,7 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S
|
||||
"description": "Run LLM dashboard validation",
|
||||
"required_entities": ["dashboard_id"],
|
||||
"optional_entities": ["dashboard_ref", "environment", "provider"],
|
||||
"defaults": {"environment": default_env_id, "provider": fallback_provider},
|
||||
"defaults": {"environment": default_env_id, "provider": validation_provider},
|
||||
"risk_level": "guarded",
|
||||
"requires_confirmation": False,
|
||||
},
|
||||
@@ -795,7 +851,7 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S
|
||||
"description": "Generate dataset documentation via LLM",
|
||||
"required_entities": ["dataset_id"],
|
||||
"optional_entities": ["environment", "provider"],
|
||||
"defaults": {"environment": default_env_id, "provider": fallback_provider},
|
||||
"defaults": {"environment": default_env_id, "provider": documentation_provider},
|
||||
"risk_level": "guarded",
|
||||
"requires_confirmation": False,
|
||||
},
|
||||
@@ -867,9 +923,13 @@ async def _plan_intent_with_llm(
|
||||
if not tools:
|
||||
return None
|
||||
|
||||
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
|
||||
planner_provider_token = llm_settings.get("assistant_planner_provider")
|
||||
planner_model_override = llm_settings.get("assistant_planner_model")
|
||||
llm_service = LLMProviderService(db)
|
||||
providers = llm_service.get_all_providers()
|
||||
provider = next((p for p in providers if p.is_active), None)
|
||||
provider_id = _resolve_provider_id(planner_provider_token, db)
|
||||
provider = next((p for p in providers if p.id == provider_id), None)
|
||||
if not provider:
|
||||
return None
|
||||
api_key = llm_service.get_decrypted_api_key(provider.id)
|
||||
@@ -880,7 +940,7 @@ async def _plan_intent_with_llm(
|
||||
provider_type=LLMProviderType(provider.provider_type),
|
||||
api_key=api_key,
|
||||
base_url=provider.base_url,
|
||||
default_model=provider.default_model,
|
||||
default_model=planner_model_override or provider.default_model,
|
||||
)
|
||||
|
||||
system_instruction = (
|
||||
@@ -983,6 +1043,29 @@ async def _dispatch_intent(
|
||||
operation = intent.get("operation")
|
||||
entities = intent.get("entities", {})
|
||||
|
||||
if operation == "show_capabilities":
|
||||
tools_catalog = _build_tool_catalog(current_user, config_manager, db)
|
||||
labels = {
|
||||
"create_branch": "Git: создание ветки",
|
||||
"commit_changes": "Git: коммит",
|
||||
"deploy_dashboard": "Git: деплой дашборда",
|
||||
"execute_migration": "Миграции: запуск переноса",
|
||||
"run_backup": "Бэкапы: запуск резервного копирования",
|
||||
"run_llm_validation": "LLM: валидация дашборда",
|
||||
"run_llm_documentation": "LLM: генерация документации",
|
||||
"get_task_status": "Статус: проверка задачи",
|
||||
}
|
||||
available = [labels[t["operation"]] for t in tools_catalog if t["operation"] in labels]
|
||||
if not available:
|
||||
return "Сейчас нет доступных для вас операций ассистента.", None, []
|
||||
commands = "\n".join(f"- {item}" for item in available)
|
||||
text = (
|
||||
"Вот что я могу сделать для вас:\n"
|
||||
f"{commands}\n\n"
|
||||
"Пример: `запусти миграцию с dev на prod для дашборда 42`."
|
||||
)
|
||||
return text, None, []
|
||||
|
||||
if operation == "get_task_status":
|
||||
_check_any_permission(current_user, [("tasks", "READ")])
|
||||
task_id = entities.get("task_id")
|
||||
@@ -1111,12 +1194,27 @@ async def _dispatch_intent(
|
||||
env_id,
|
||||
config_manager,
|
||||
)
|
||||
provider_id = _resolve_provider_id(entities.get("provider"), db)
|
||||
provider_id = _resolve_provider_id(
|
||||
entities.get("provider"),
|
||||
db,
|
||||
config_manager=config_manager,
|
||||
task_key="dashboard_validation",
|
||||
)
|
||||
if not dashboard_id or not env_id or not provider_id:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="Missing dashboard_id/environment/provider. Укажите ID/slug дашборда или окружение.",
|
||||
)
|
||||
provider = LLMProviderService(db).get_provider(provider_id)
|
||||
provider_model = provider.default_model if provider else ""
|
||||
if not is_multimodal_model(provider_model):
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=(
|
||||
"Selected provider model is not multimodal for dashboard validation. "
|
||||
"Выберите мультимодальную модель (например, gpt-4o)."
|
||||
),
|
||||
)
|
||||
|
||||
task = await task_manager.create_task(
|
||||
plugin_id="llm_dashboard_validation",
|
||||
@@ -1140,7 +1238,12 @@ async def _dispatch_intent(
|
||||
_check_any_permission(current_user, [("plugin:llm_documentation", "EXECUTE")])
|
||||
dataset_id = entities.get("dataset_id")
|
||||
env_id = _resolve_env_id(entities.get("environment"), config_manager)
|
||||
provider_id = _resolve_provider_id(entities.get("provider"), db)
|
||||
provider_id = _resolve_provider_id(
|
||||
entities.get("provider"),
|
||||
db,
|
||||
config_manager=config_manager,
|
||||
task_key="documentation",
|
||||
)
|
||||
if not dataset_id or not env_id or not provider_id:
|
||||
raise HTTPException(status_code=400, detail="Missing dataset_id/environment/provider")
|
||||
|
||||
@@ -1301,6 +1404,7 @@ async def send_message(
|
||||
is_clarification_error = exc.status_code in (400, 422) and (
|
||||
detail_text.lower().startswith("missing")
|
||||
or "укажите" in detail_text.lower()
|
||||
or "выберите" in detail_text.lower()
|
||||
)
|
||||
if exc.status_code == status.HTTP_403_FORBIDDEN:
|
||||
state = "denied"
|
||||
|
||||
@@ -25,7 +25,11 @@ from src.api.routes.git_schemas import (
|
||||
)
|
||||
from src.services.git_service import GitService
|
||||
from src.core.logger import logger, belief_scope
|
||||
from ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, normalize_llm_settings
|
||||
from ...services.llm_prompt_templates import (
|
||||
DEFAULT_LLM_PROMPTS,
|
||||
normalize_llm_settings,
|
||||
resolve_bound_provider_id,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["git"])
|
||||
git_service = GitService()
|
||||
@@ -431,7 +435,11 @@ async def generate_commit_message(
|
||||
|
||||
llm_service = LLMProviderService(db)
|
||||
providers = llm_service.get_all_providers()
|
||||
provider = next((p for p in providers if p.is_active), None)
|
||||
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
|
||||
bound_provider_id = resolve_bound_provider_id(llm_settings, "git_commit")
|
||||
provider = next((p for p in providers if p.id == bound_provider_id), None)
|
||||
if not provider:
|
||||
provider = next((p for p in providers if p.is_active), None)
|
||||
|
||||
if not provider:
|
||||
raise HTTPException(status_code=400, detail="No active LLM provider found")
|
||||
@@ -447,7 +455,6 @@ async def generate_commit_message(
|
||||
# 4. Generate Message
|
||||
from ...plugins.git.llm_extension import GitLLMExtension
|
||||
extension = GitLLMExtension(client)
|
||||
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
|
||||
git_prompt = llm_settings["prompts"].get(
|
||||
"git_commit_prompt",
|
||||
DEFAULT_LLM_PROMPTS["git_commit_prompt"],
|
||||
|
||||
@@ -11,7 +11,13 @@ from ...core.logger import belief_scope
|
||||
|
||||
from ...core.task_manager import TaskManager, Task, TaskStatus, LogEntry
|
||||
from ...core.task_manager.models import LogFilter, LogStats
|
||||
from ...dependencies import get_task_manager, has_permission, get_current_user
|
||||
from ...dependencies import get_task_manager, has_permission, get_current_user, get_config_manager
|
||||
from ...core.config_manager import ConfigManager
|
||||
from ...services.llm_prompt_templates import (
|
||||
is_multimodal_model,
|
||||
normalize_llm_settings,
|
||||
resolve_bound_provider_id,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -42,7 +48,8 @@ class ResumeTaskRequest(BaseModel):
|
||||
async def create_task(
|
||||
request: CreateTaskRequest,
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
current_user = Depends(get_current_user)
|
||||
current_user = Depends(get_current_user),
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
):
|
||||
# Dynamic permission check based on plugin_id
|
||||
has_permission(f"plugin:{request.plugin_id}", "EXECUTE")(current_user)
|
||||
@@ -51,18 +58,35 @@ async def create_task(
|
||||
"""
|
||||
with belief_scope("create_task"):
|
||||
try:
|
||||
# Special handling for validation task to include provider config
|
||||
if request.plugin_id == "llm_dashboard_validation":
|
||||
# Special handling for LLM tasks to resolve provider config by task binding.
|
||||
if request.plugin_id in {"llm_dashboard_validation", "llm_documentation"}:
|
||||
from ...core.database import SessionLocal
|
||||
from ...services.llm_provider import LLMProviderService
|
||||
db = SessionLocal()
|
||||
try:
|
||||
llm_service = LLMProviderService(db)
|
||||
provider_id = request.params.get("provider_id")
|
||||
if not provider_id:
|
||||
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
|
||||
binding_key = "dashboard_validation" if request.plugin_id == "llm_dashboard_validation" else "documentation"
|
||||
provider_id = resolve_bound_provider_id(llm_settings, binding_key)
|
||||
if provider_id:
|
||||
request.params["provider_id"] = provider_id
|
||||
if not provider_id:
|
||||
providers = llm_service.get_all_providers()
|
||||
active_provider = next((p for p in providers if p.is_active), None)
|
||||
if active_provider:
|
||||
provider_id = active_provider.id
|
||||
request.params["provider_id"] = provider_id
|
||||
|
||||
if provider_id:
|
||||
db_provider = llm_service.get_provider(provider_id)
|
||||
if not db_provider:
|
||||
raise ValueError(f"LLM Provider {provider_id} not found")
|
||||
if request.plugin_id == "llm_dashboard_validation" and not is_multimodal_model(db_provider.default_model):
|
||||
raise ValueError(
|
||||
"Selected provider model is not multimodal for dashboard validation"
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from ..models.storage import StorageConfig
|
||||
from ..services.llm_prompt_templates import DEFAULT_LLM_PROMPTS
|
||||
from ..services.llm_prompt_templates import (
|
||||
DEFAULT_LLM_ASSISTANT_SETTINGS,
|
||||
DEFAULT_LLM_PROMPTS,
|
||||
DEFAULT_LLM_PROVIDER_BINDINGS,
|
||||
)
|
||||
|
||||
# [DEF:Schedule:DataClass]
|
||||
# @PURPOSE: Represents a backup schedule configuration.
|
||||
@@ -55,6 +59,8 @@ class GlobalSettings(BaseModel):
|
||||
"providers": [],
|
||||
"default_provider": "",
|
||||
"prompts": dict(DEFAULT_LLM_PROMPTS),
|
||||
"provider_bindings": dict(DEFAULT_LLM_PROVIDER_BINDINGS),
|
||||
**dict(DEFAULT_LLM_ASSISTANT_SETTINGS),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from ...models.llm import ValidationRecord
|
||||
from ...core.task_manager.context import TaskContext
|
||||
from ...services.llm_prompt_templates import (
|
||||
DEFAULT_LLM_PROMPTS,
|
||||
is_multimodal_model,
|
||||
normalize_llm_settings,
|
||||
render_prompt,
|
||||
)
|
||||
@@ -108,6 +109,10 @@ class DashboardValidationPlugin(PluginBase):
|
||||
llm_log.debug(f" Base URL: {db_provider.base_url}")
|
||||
llm_log.debug(f" Default Model: {db_provider.default_model}")
|
||||
llm_log.debug(f" Is Active: {db_provider.is_active}")
|
||||
if not is_multimodal_model(db_provider.default_model):
|
||||
raise ValueError(
|
||||
"Dashboard validation requires a multimodal model (image input support)."
|
||||
)
|
||||
|
||||
api_key = llm_service.get_decrypted_api_key(provider_id)
|
||||
llm_log.debug(f"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...")
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
# @INVARIANT: All required prompt keys remain available after normalization.
|
||||
|
||||
from src.services.llm_prompt_templates import (
|
||||
DEFAULT_LLM_ASSISTANT_SETTINGS,
|
||||
DEFAULT_LLM_PROVIDER_BINDINGS,
|
||||
DEFAULT_LLM_PROMPTS,
|
||||
is_multimodal_model,
|
||||
normalize_llm_settings,
|
||||
resolve_bound_provider_id,
|
||||
render_prompt,
|
||||
)
|
||||
|
||||
@@ -22,10 +26,15 @@ def test_normalize_llm_settings_adds_default_prompts():
|
||||
normalized = normalize_llm_settings({"default_provider": "x"})
|
||||
|
||||
assert "prompts" in normalized
|
||||
assert "provider_bindings" in normalized
|
||||
assert normalized["default_provider"] == "x"
|
||||
for key in DEFAULT_LLM_PROMPTS:
|
||||
assert key in normalized["prompts"]
|
||||
assert isinstance(normalized["prompts"][key], str)
|
||||
for key in DEFAULT_LLM_PROVIDER_BINDINGS:
|
||||
assert key in normalized["provider_bindings"]
|
||||
for key in DEFAULT_LLM_ASSISTANT_SETTINGS:
|
||||
assert key in normalized
|
||||
# [/DEF:test_normalize_llm_settings_adds_default_prompts:Function]
|
||||
|
||||
|
||||
@@ -59,4 +68,42 @@ def test_render_prompt_replaces_known_placeholders():
|
||||
# [/DEF:test_render_prompt_replaces_known_placeholders:Function]
|
||||
|
||||
|
||||
# [DEF:test_is_multimodal_model_detects_known_vision_models:Function]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names.
|
||||
def test_is_multimodal_model_detects_known_vision_models():
|
||||
assert is_multimodal_model("gpt-4o") is True
|
||||
assert is_multimodal_model("claude-3-5-sonnet") is True
|
||||
assert is_multimodal_model("text-only-model") is False
|
||||
# [/DEF:test_is_multimodal_model_detects_known_vision_models:Function]
|
||||
|
||||
|
||||
# [DEF:test_resolve_bound_provider_id_prefers_binding_then_default:Function]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Verify provider binding resolution priority.
|
||||
def test_resolve_bound_provider_id_prefers_binding_then_default():
|
||||
settings = {
|
||||
"default_provider": "default-1",
|
||||
"provider_bindings": {"dashboard_validation": "vision-1"},
|
||||
}
|
||||
assert resolve_bound_provider_id(settings, "dashboard_validation") == "vision-1"
|
||||
assert resolve_bound_provider_id(settings, "documentation") == "default-1"
|
||||
# [/DEF:test_resolve_bound_provider_id_prefers_binding_then_default:Function]
|
||||
|
||||
|
||||
# [DEF:test_normalize_llm_settings_keeps_assistant_planner_settings:Function]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized.
|
||||
def test_normalize_llm_settings_keeps_assistant_planner_settings():
|
||||
normalized = normalize_llm_settings(
|
||||
{
|
||||
"assistant_planner_provider": "provider-a",
|
||||
"assistant_planner_model": "gpt-4.1-mini",
|
||||
}
|
||||
)
|
||||
assert normalized["assistant_planner_provider"] == "provider-a"
|
||||
assert normalized["assistant_planner_model"] == "gpt-4.1-mini"
|
||||
# [/DEF:test_normalize_llm_settings_keeps_assistant_planner_settings:Function]
|
||||
|
||||
|
||||
# [/DEF:backend.src.services.__tests__.test_llm_prompt_templates:Module]
|
||||
|
||||
@@ -61,23 +61,109 @@ DEFAULT_LLM_PROMPTS: Dict[str, str] = {
|
||||
# [/DEF:DEFAULT_LLM_PROMPTS:Constant]
|
||||
|
||||
|
||||
# [DEF:DEFAULT_LLM_PROVIDER_BINDINGS:Constant]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Default provider binding per task domain.
|
||||
DEFAULT_LLM_PROVIDER_BINDINGS: Dict[str, str] = {
|
||||
"dashboard_validation": "",
|
||||
"documentation": "",
|
||||
"git_commit": "",
|
||||
}
|
||||
# [/DEF:DEFAULT_LLM_PROVIDER_BINDINGS:Constant]
|
||||
|
||||
|
||||
# [DEF:DEFAULT_LLM_ASSISTANT_SETTINGS:Constant]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Default planner settings for assistant chat intent model/provider resolution.
|
||||
DEFAULT_LLM_ASSISTANT_SETTINGS: Dict[str, str] = {
|
||||
"assistant_planner_provider": "",
|
||||
"assistant_planner_model": "",
|
||||
}
|
||||
# [/DEF:DEFAULT_LLM_ASSISTANT_SETTINGS:Constant]
|
||||
|
||||
|
||||
# [DEF:normalize_llm_settings:Function]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Ensure llm settings contain stable schema with prompts section and default templates.
|
||||
# @PRE: llm_settings is dictionary-like value or None.
|
||||
# @POST: Returned dict contains prompts with all required template keys.
|
||||
def normalize_llm_settings(llm_settings: Any) -> Dict[str, Any]:
|
||||
normalized: Dict[str, Any] = {"providers": [], "default_provider": "", "prompts": {}}
|
||||
normalized: Dict[str, Any] = {
|
||||
"providers": [],
|
||||
"default_provider": "",
|
||||
"prompts": {},
|
||||
"provider_bindings": {},
|
||||
**DEFAULT_LLM_ASSISTANT_SETTINGS,
|
||||
}
|
||||
if isinstance(llm_settings, dict):
|
||||
normalized.update({k: v for k, v in llm_settings.items() if k in ("providers", "default_provider", "prompts")})
|
||||
normalized.update(
|
||||
{
|
||||
k: v
|
||||
for k, v in llm_settings.items()
|
||||
if k
|
||||
in (
|
||||
"providers",
|
||||
"default_provider",
|
||||
"prompts",
|
||||
"provider_bindings",
|
||||
"assistant_planner_provider",
|
||||
"assistant_planner_model",
|
||||
)
|
||||
}
|
||||
)
|
||||
prompts = normalized.get("prompts") if isinstance(normalized.get("prompts"), dict) else {}
|
||||
merged_prompts = deepcopy(DEFAULT_LLM_PROMPTS)
|
||||
merged_prompts.update({k: v for k, v in prompts.items() if isinstance(v, str) and v.strip()})
|
||||
normalized["prompts"] = merged_prompts
|
||||
bindings = normalized.get("provider_bindings") if isinstance(normalized.get("provider_bindings"), dict) else {}
|
||||
merged_bindings = deepcopy(DEFAULT_LLM_PROVIDER_BINDINGS)
|
||||
merged_bindings.update({k: v for k, v in bindings.items() if isinstance(v, str)})
|
||||
normalized["provider_bindings"] = merged_bindings
|
||||
for key, default_value in DEFAULT_LLM_ASSISTANT_SETTINGS.items():
|
||||
value = normalized.get(key, default_value)
|
||||
normalized[key] = value.strip() if isinstance(value, str) else default_value
|
||||
return normalized
|
||||
# [/DEF:normalize_llm_settings:Function]
|
||||
|
||||
|
||||
# [DEF:is_multimodal_model:Function]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Heuristically determine whether model supports image input required for dashboard validation.
|
||||
# @PRE: model_name may be empty or mixed-case.
|
||||
# @POST: Returns True when model likely supports multimodal input.
|
||||
def is_multimodal_model(model_name: str) -> bool:
|
||||
token = (model_name or "").strip().lower()
|
||||
if not token:
|
||||
return False
|
||||
multimodal_markers = (
|
||||
"gpt-4o",
|
||||
"gpt-4.1",
|
||||
"vision",
|
||||
"vl",
|
||||
"gemini",
|
||||
"claude-3",
|
||||
"claude-sonnet-4",
|
||||
)
|
||||
return any(marker in token for marker in multimodal_markers)
|
||||
# [/DEF:is_multimodal_model:Function]
|
||||
|
||||
|
||||
# [DEF:resolve_bound_provider_id:Function]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Resolve provider id configured for a task binding with fallback to default provider.
|
||||
# @PRE: llm_settings is normalized or raw dict from config.
|
||||
# @POST: Returns configured provider id or fallback id/empty string when not defined.
|
||||
def resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str:
|
||||
normalized = normalize_llm_settings(llm_settings)
|
||||
bindings = normalized.get("provider_bindings", {})
|
||||
bound = bindings.get(task_key)
|
||||
if isinstance(bound, str) and bound.strip():
|
||||
return bound.strip()
|
||||
default_provider = normalized.get("default_provider", "")
|
||||
return default_provider.strip() if isinstance(default_provider, str) else ""
|
||||
# [/DEF:resolve_bound_provider_id:Function]
|
||||
|
||||
|
||||
# [DEF:render_prompt:Function]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Render prompt template using deterministic placeholder replacement with graceful fallback.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<script lang="ts">
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount, createEventDispatcher } from 'svelte';
|
||||
import { t } from '../lib/i18n';
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
@@ -48,7 +49,7 @@
|
||||
value={selectedId}
|
||||
on:change={handleSelect}
|
||||
>
|
||||
<option value="" disabled>-- Choose an environment --</option>
|
||||
<option value="" disabled>{$t.common?.choose_environment || "-- Choose an environment --"}</option>
|
||||
{#each environments as env}
|
||||
<option value={env.id}>{env.name} ({env.url})</option>
|
||||
{/each}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { gitService } from "../../services/gitService";
|
||||
import { addToast as toast } from "../../lib/toasts.js";
|
||||
import { api } from "../../lib/api";
|
||||
import { t } from "../../lib/i18n";
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
@@ -49,10 +50,10 @@
|
||||
`/git/repositories/${dashboardId}/generate-message`,
|
||||
);
|
||||
message = data.message;
|
||||
toast("Commit message generated", "success");
|
||||
toast($t.git?.commit_message_generated || "Commit message generated", "success");
|
||||
} catch (e) {
|
||||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||||
toast(e.message || "Failed to generate message", "error");
|
||||
toast(e.message || ($t.git?.commit_message_failed || "Failed to generate message"), "error");
|
||||
} finally {
|
||||
generatingMessage = false;
|
||||
}
|
||||
@@ -93,7 +94,7 @@
|
||||
if (!diff) diff = "";
|
||||
} catch (e) {
|
||||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||||
toast("Failed to load changes", "error");
|
||||
toast($t.git?.load_changes_failed || "Failed to load changes", "error");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -114,7 +115,7 @@
|
||||
committing = true;
|
||||
try {
|
||||
await gitService.commit(dashboardId, message, []);
|
||||
toast("Changes committed successfully", "success");
|
||||
toast($t.git?.commit_success || "Changes committed successfully", "success");
|
||||
dispatch("commit");
|
||||
show = false;
|
||||
message = "";
|
||||
@@ -141,7 +142,7 @@
|
||||
<div
|
||||
class="bg-white p-6 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col"
|
||||
>
|
||||
<h2 class="text-xl font-bold mb-4">Commit Changes</h2>
|
||||
<h2 class="text-xl font-bold mb-4">{$t.git?.commit || "Commit Changes"}</h2>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4 flex-1 overflow-hidden">
|
||||
<!-- Left: Message and Files -->
|
||||
@@ -150,7 +151,7 @@
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Commit Message</label
|
||||
>{$t.git?.commit_message || "Commit Message"}</label
|
||||
>
|
||||
<button
|
||||
onclick={handleGenerateMessage}
|
||||
@@ -158,16 +159,16 @@
|
||||
class="text-xs text-blue-600 hover:text-blue-800 disabled:opacity-50 flex items-center"
|
||||
>
|
||||
{#if generatingMessage}
|
||||
<span class="animate-spin mr-1">↻</span> Generating...
|
||||
<span class="animate-spin mr-1">↻</span> {$t.mapper?.generating || "Generating..."}
|
||||
{:else}
|
||||
<span class="mr-1">✨</span> Generate with AI
|
||||
<span class="mr-1">✨</span> {$t.git?.generate_with_ai || "Generate with AI"}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
class="w-full border rounded p-2 h-32 focus:ring-2 focus:ring-blue-500 outline-none resize-none"
|
||||
placeholder="Describe your changes..."
|
||||
placeholder={$t.git?.describe_changes || "Describe your changes..."}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@@ -176,7 +177,7 @@
|
||||
<h3
|
||||
class="text-sm font-bold text-gray-500 uppercase mb-2"
|
||||
>
|
||||
Changed Files
|
||||
{$t.git?.changed_files || "Changed Files"}
|
||||
</h3>
|
||||
<ul class="text-xs space-y-1">
|
||||
{#each status.staged_files as file}
|
||||
@@ -218,14 +219,14 @@
|
||||
<div
|
||||
class="bg-gray-200 px-3 py-1 text-xs font-bold text-gray-600 border-b"
|
||||
>
|
||||
Changes Preview
|
||||
{$t.git?.changes_preview || "Changes Preview"}
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto p-2">
|
||||
{#if loading}
|
||||
<div
|
||||
class="flex items-center justify-center h-full text-gray-500"
|
||||
>
|
||||
Loading diff...
|
||||
{$t.git?.loading_diff || "Loading diff..."}
|
||||
</div>
|
||||
{:else if diff}
|
||||
<pre
|
||||
@@ -234,7 +235,7 @@
|
||||
<div
|
||||
class="flex items-center justify-center h-full text-gray-500 italic"
|
||||
>
|
||||
No changes detected
|
||||
{$t.git?.no_changes || "No changes detected"}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -246,7 +247,7 @@
|
||||
onclick={() => (show = false)}
|
||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
Cancel
|
||||
{$t.common?.cancel || "Cancel"}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleCommit}
|
||||
@@ -257,7 +258,7 @@
|
||||
status?.staged_files?.length === 0)}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{committing ? "Committing..." : "Commit"}
|
||||
{committing ? ($t.git?.committing || "Committing...") : ($t.git?.commit || "Commit")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import { gitService } from "../../services/gitService";
|
||||
import { addToast as toast } from "../../lib/toasts.js";
|
||||
import { t } from "../../lib/i18n";
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
@@ -55,7 +56,7 @@
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(`[DeploymentModal][Coherence:Failed] ${e.message}`);
|
||||
toast("Failed to load environments", "error");
|
||||
toast($t.migration?.loading_envs_failed || "Failed to load environments", "error");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -76,7 +77,7 @@
|
||||
try {
|
||||
const result = await gitService.deploy(dashboardId, selectedEnv);
|
||||
toast(
|
||||
result.message || "Deployment triggered successfully",
|
||||
result.message || ($t.git?.deploy_success || "Deployment triggered successfully"),
|
||||
"success",
|
||||
);
|
||||
dispatch("deploy");
|
||||
@@ -98,26 +99,26 @@
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="bg-white p-6 rounded-lg shadow-xl w-96">
|
||||
<h2 class="text-xl font-bold mb-4">Deploy Dashboard</h2>
|
||||
<h2 class="text-xl font-bold mb-4">{$t.git?.deploy || "Deploy Dashboard"}</h2>
|
||||
|
||||
{#if loading}
|
||||
<p class="text-gray-500">Loading environments...</p>
|
||||
<p class="text-gray-500">{$t.migration?.loading_envs || "Loading environments..."}</p>
|
||||
{:else if environments.length === 0}
|
||||
<p class="text-red-500 mb-4">
|
||||
No deployment environments configured.
|
||||
{$t.git?.no_deploy_envs || "No deployment environments configured."}
|
||||
</p>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
onclick={() => (show = false)}
|
||||
class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
|
||||
>
|
||||
Close
|
||||
{$t.common?.close || "Close"}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2"
|
||||
>Select Target Environment</label
|
||||
>{$t.migration?.target_env || "Select Target Environment"}</label
|
||||
>
|
||||
<select
|
||||
bind:value={selectedEnv}
|
||||
@@ -136,7 +137,7 @@
|
||||
onclick={() => (show = false)}
|
||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
Cancel
|
||||
{$t.common?.cancel || "Cancel"}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleDeploy}
|
||||
@@ -164,9 +165,9 @@
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Deploying...
|
||||
{$t.git?.deploying || "Deploying..."}
|
||||
{:else}
|
||||
Deploy
|
||||
{$t.git?.deploy || "Deploy"}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -82,13 +82,13 @@
|
||||
*/
|
||||
async function handleInit() {
|
||||
if (!selectedConfigId || !remoteUrl) {
|
||||
toast('Please select a Git server and provide remote URL', 'error');
|
||||
toast($t.git?.init_validation_error || 'Please select a Git server and provide remote URL', 'error');
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
try {
|
||||
await gitService.initRepository(dashboardId, selectedConfigId, remoteUrl);
|
||||
toast('Repository initialized successfully', 'success');
|
||||
toast($t.git?.init_success || 'Repository initialized successfully', 'success');
|
||||
initialized = true;
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
@@ -110,7 +110,7 @@
|
||||
// Try to get selected environment from localStorage (set by EnvSelector)
|
||||
const sourceEnvId = localStorage.getItem('selected_env_id');
|
||||
await gitService.sync(dashboardId, sourceEnvId);
|
||||
toast('Dashboard state synced to Git', 'success');
|
||||
toast($t.git?.sync_success || 'Dashboard state synced to Git', 'success');
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
@@ -129,7 +129,7 @@
|
||||
loading = true;
|
||||
try {
|
||||
await gitService.push(dashboardId);
|
||||
toast('Changes pushed to remote', 'success');
|
||||
toast($t.git?.push_success || 'Changes pushed to remote', 'success');
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
@@ -148,7 +148,7 @@
|
||||
loading = true;
|
||||
try {
|
||||
await gitService.pull(dashboardId);
|
||||
toast('Changes pulled from remote', 'success');
|
||||
toast($t.git?.pull_success || 'Changes pulled from remote', 'success');
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
@@ -164,8 +164,8 @@
|
||||
{#if show}
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white p-6 rounded-lg shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<PageHeader title="{$t.git.management}: {dashboardTitle}">
|
||||
<div slot="subtitle" class="text-sm text-gray-500">ID: {dashboardId}</div>
|
||||
<PageHeader title={`${$t.git?.management || "Git Management"}: ${dashboardTitle}`}>
|
||||
<div slot="subtitle" class="text-sm text-gray-500">{$t.common?.id || "ID"}: {dashboardId}</div>
|
||||
<div slot="actions">
|
||||
<button onclick={() => show = false} class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -193,13 +193,13 @@
|
||||
options={configs.map(c => ({ value: c.id, label: `${c.name} (${c.provider})` }))}
|
||||
/>
|
||||
{#if configs.length === 0}
|
||||
<p class="text-xs text-red-500 -mt-4">No Git servers configured. Go to Settings -> Git to add one.</p>
|
||||
<p class="text-xs text-red-500 -mt-4">{$t.git?.no_servers_configured || "No Git servers configured. Go to Settings -> Git to add one."}</p>
|
||||
{/if}
|
||||
|
||||
<Input
|
||||
label={$t.git.remote_url}
|
||||
bind:value={remoteUrl}
|
||||
placeholder="https://github.com/org/repo.git"
|
||||
placeholder={$t.git?.remote_url_placeholder || "https://github.com/org/repo.git"}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -29,6 +29,20 @@
|
||||
let testStatus = { type: "", message: "" };
|
||||
let isTesting = false;
|
||||
|
||||
function isMultimodalModel(modelName) {
|
||||
const token = (modelName || "").toLowerCase();
|
||||
if (!token) return false;
|
||||
return (
|
||||
token.includes("gpt-4o") ||
|
||||
token.includes("gpt-4.1") ||
|
||||
token.includes("vision") ||
|
||||
token.includes("vl") ||
|
||||
token.includes("gemini") ||
|
||||
token.includes("claude-3") ||
|
||||
token.includes("claude-sonnet-4")
|
||||
);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData = {
|
||||
name: "",
|
||||
@@ -295,6 +309,13 @@
|
||||
>
|
||||
{provider.is_active ? $t.llm.active : "Inactive"}
|
||||
</span>
|
||||
<span
|
||||
class={`text-xs px-2 py-0.5 rounded-full ${isMultimodalModel(provider.default_model) ? "bg-sky-100 text-sky-800" : "bg-amber-100 text-amber-800"}`}
|
||||
>
|
||||
{isMultimodalModel(provider.default_model)
|
||||
? ($t.llm?.multimodal || "Multimodal")
|
||||
: ($t.llm?.text_only || "Text only")}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{provider.provider_type} • {provider.default_model}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
// @POST: A new connection is created via the connection service and a success event is dispatched.
|
||||
async function handleSubmit() {
|
||||
if (!name || !host || !database || !username || !password) {
|
||||
addToast('Please fill in all required fields', 'warning');
|
||||
addToast($t.connections?.required_fields || 'Please fill in all required fields', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
const newConnection = await createConnection({
|
||||
name, type, host, port: Number(port), database, username, password
|
||||
});
|
||||
addToast('Connection created successfully', 'success');
|
||||
addToast($t.connections?.created_success || 'Connection created successfully', 'success');
|
||||
dispatch('success', newConnection);
|
||||
resetForm();
|
||||
} catch (e) {
|
||||
@@ -70,10 +70,10 @@
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<Card title={$t.connections?.add_new || "Add New Connection"}>
|
||||
<form on:submit|preventDefault={handleSubmit} class="space-y-6">
|
||||
<Input label={$t.connections?.name || "Connection Name"} bind:value={name} placeholder="e.g. Production DWH" />
|
||||
<Input label={$t.connections?.name || "Connection Name"} bind:value={name} placeholder={$t.connections?.name_placeholder || "e.g. Production DWH"} />
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Input label={$t.connections?.host || "Host"} bind:value={host} placeholder="10.0.0.1" />
|
||||
<Input label={$t.connections?.host || "Host"} bind:value={host} placeholder={$t.connections?.host_placeholder || "10.0.0.1"} />
|
||||
<Input label={$t.connections?.port || "Port"} type="number" bind:value={port} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
try {
|
||||
connections = await getConnections();
|
||||
} catch (e) {
|
||||
addToast('Failed to fetch connections', 'error');
|
||||
addToast($t.connections?.fetch_failed || 'Failed to fetch connections', 'error');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -40,11 +40,11 @@
|
||||
// @PRE: id is provided and user confirms deletion.
|
||||
// @POST: Connection is deleted from backend and list is reloaded.
|
||||
async function handleDelete(id) {
|
||||
if (!confirm('Are you sure you want to delete this connection?')) return;
|
||||
if (!confirm($t.connections?.delete_confirm || 'Are you sure you want to delete this connection?')) return;
|
||||
|
||||
try {
|
||||
await deleteConnection(id);
|
||||
addToast('Connection deleted', 'success');
|
||||
addToast($t.connections?.deleted_success || 'Connection deleted', 'success');
|
||||
await fetchConnections();
|
||||
} catch (e) {
|
||||
addToast(e.message, 'error');
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { runTask, getTaskStatus } from '../../services/toolsService.js';
|
||||
import { selectedTask } from '../../lib/stores.js';
|
||||
import { addToast } from '../../lib/toasts.js';
|
||||
import { t } from '../../lib/i18n';
|
||||
// [/SECTION]
|
||||
|
||||
let envs = [];
|
||||
@@ -35,7 +36,7 @@
|
||||
try {
|
||||
envs = await api.getEnvironmentsList();
|
||||
} catch (e) {
|
||||
addToast('Failed to fetch environments', 'error');
|
||||
addToast($t.debug?.fetch_env_failed || 'Failed to fetch environments', 'error');
|
||||
}
|
||||
}
|
||||
// [/DEF:fetchEnvironments:Function]
|
||||
@@ -54,7 +55,7 @@
|
||||
let params = { action };
|
||||
if (action === 'test-db-api') {
|
||||
if (!sourceEnv || !targetEnv) {
|
||||
addToast('Source and Target environments are required', 'warning');
|
||||
addToast($t.debug?.source_target_required || 'Source and Target environments are required', 'warning');
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
@@ -64,7 +65,7 @@
|
||||
params.target_env = tEnv.name;
|
||||
} else {
|
||||
if (!selectedEnv || !datasetId) {
|
||||
addToast('Environment and Dataset ID are required', 'warning');
|
||||
addToast($t.debug?.env_dataset_required || 'Environment and Dataset ID are required', 'warning');
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
@@ -101,11 +102,11 @@
|
||||
clearInterval(pollInterval);
|
||||
isRunning = false;
|
||||
results = task.result;
|
||||
addToast('Debug task completed', 'success');
|
||||
addToast($t.debug?.completed || 'Debug task completed', 'success');
|
||||
} else if (task.status === 'FAILED') {
|
||||
clearInterval(pollInterval);
|
||||
isRunning = false;
|
||||
addToast('Debug task failed', 'error');
|
||||
addToast($t.debug?.failed || 'Debug task failed', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(pollInterval);
|
||||
@@ -120,31 +121,31 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">System Diagnostics</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">{$t.debug?.title || 'System Diagnostics'}</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Debug Action</label>
|
||||
<label class="block text-sm font-medium text-gray-700">{$t.debug?.action || 'Debug Action'}</label>
|
||||
<select bind:value={action} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<option value="test-db-api">Test Database API (Compare Envs)</option>
|
||||
<option value="get-dataset-structure">Get Dataset Structure (JSON)</option>
|
||||
<option value="test-db-api">{$t.debug?.test_db_api || 'Test Database API (Compare Envs)'}</option>
|
||||
<option value="get-dataset-structure">{$t.debug?.get_dataset_structure || 'Get Dataset Structure (JSON)'}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#if action === 'test-db-api'}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="src-env" class="block text-sm font-medium text-gray-700">Source Environment</label>
|
||||
<label for="src-env" class="block text-sm font-medium text-gray-700">{$t.migration?.source_env || 'Source Environment'}</label>
|
||||
<select id="src-env" bind:value={sourceEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<option value="" disabled>-- Select Source --</option>
|
||||
<option value="" disabled>{$t.debug?.select_source || '-- Select Source --'}</option>
|
||||
{#each envs as env}
|
||||
<option value={env.id}>{env.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tgt-env" class="block text-sm font-medium text-gray-700">Target Environment</label>
|
||||
<label for="tgt-env" class="block text-sm font-medium text-gray-700">{$t.migration?.target_env || 'Target Environment'}</label>
|
||||
<select id="tgt-env" bind:value={targetEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<option value="" disabled>-- Select Target --</option>
|
||||
<option value="" disabled>{$t.debug?.select_target || '-- Select Target --'}</option>
|
||||
{#each envs as env}
|
||||
<option value={env.id}>{env.name}</option>
|
||||
{/each}
|
||||
@@ -154,16 +155,16 @@
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="debug-env" class="block text-sm font-medium text-gray-700">Environment</label>
|
||||
<label for="debug-env" class="block text-sm font-medium text-gray-700">{$t.dashboard?.environment || 'Environment'}</label>
|
||||
<select id="debug-env" bind:value={selectedEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<option value="" disabled>-- Select Environment --</option>
|
||||
<option value="" disabled>{$t.common?.choose_environment || '-- Select Environment --'}</option>
|
||||
{#each envs as env}
|
||||
<option value={env.id}>{env.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="debug-ds-id" class="block text-sm font-medium text-gray-700">Dataset ID</label>
|
||||
<label for="debug-ds-id" class="block text-sm font-medium text-gray-700">{$t.mapper?.dataset_id || 'Dataset ID'}</label>
|
||||
<input type="number" id="debug-ds-id" bind:value={datasetId} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +172,7 @@
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button on:click={handleRunDebug} disabled={isRunning} class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50">
|
||||
{isRunning ? 'Running...' : 'Run Diagnostics'}
|
||||
{isRunning ? ($t.dashboard?.running || 'Running...') : ($t.debug?.run || 'Run Diagnostics')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,7 +180,7 @@
|
||||
{#if results}
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md border border-gray-200">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">Debug Output</h3>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">{$t.debug?.output || 'Debug Output'}</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<pre class="text-xs text-gray-600 bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto h-96">{JSON.stringify(results, null, 2)}</pre>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
const activeProvider = providers.find(p => p.is_active);
|
||||
|
||||
if (!activeProvider) {
|
||||
addToast('No active LLM provider found', 'error');
|
||||
addToast($t.mapper?.errors?.no_active_llm_provider || 'No active LLM provider found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,9 +117,9 @@
|
||||
});
|
||||
|
||||
selectedTask.set(task);
|
||||
addToast('Documentation generation started', 'success');
|
||||
addToast($t.mapper?.success?.docs_started || 'Documentation generation started', 'success');
|
||||
} catch (e) {
|
||||
addToast(e.message || 'Failed to start documentation generation', 'error');
|
||||
addToast(e.message || $t.mapper?.errors?.docs_start_failed || 'Failed to start documentation generation', 'error');
|
||||
} finally {
|
||||
isGeneratingDocs = false;
|
||||
}
|
||||
@@ -130,9 +130,9 @@
|
||||
try {
|
||||
await api.put(`/mappings/datasets/${datasetId}/metadata`, doc);
|
||||
generatedDoc = null;
|
||||
addToast('Documentation applied successfully', 'success');
|
||||
addToast($t.mapper?.success?.docs_applied || 'Documentation applied successfully', 'success');
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Failed to apply documentation', 'error');
|
||||
addToast(err.message || $t.mapper?.errors?.docs_apply_failed || 'Failed to apply documentation', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
label={$t.mapper.excel_path}
|
||||
type="text"
|
||||
bind:value={excelPath}
|
||||
placeholder="/path/to/mapping.xlsx"
|
||||
placeholder={$t.mapper?.excel_placeholder || "/path/to/mapping.xlsx"}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -224,9 +224,9 @@
|
||||
disabled={isGeneratingDocs || isRunning}
|
||||
>
|
||||
{#if isGeneratingDocs}
|
||||
<span class="animate-spin mr-1">↻</span> Generating...
|
||||
<span class="animate-spin mr-1">↻</span> {$t.mapper?.generating || "Generating..."}
|
||||
{:else}
|
||||
<span class="mr-1">✨</span> Generate Docs
|
||||
<span class="mr-1">✨</span> {$t.datasets?.generate_docs || "Generate Docs"}
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
*/
|
||||
function getBreadcrumbs(pathname, maxVisible = 3) {
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
const allItems = [{ label: "Home", path: "/" }];
|
||||
const allItems = [{ label: $t.nav?.home || "Home", path: "/" }];
|
||||
|
||||
let currentPath = "";
|
||||
segments.forEach((segment, index) => {
|
||||
@@ -136,7 +136,7 @@
|
||||
|
||||
<nav
|
||||
class="mx-4 md:mx-6"
|
||||
aria-label="Breadcrumb navigation"
|
||||
aria-label={$t.nav?.breadcrumb_nav || "Breadcrumb navigation"}
|
||||
>
|
||||
<div class="inline-flex max-w-full items-center gap-1.5 rounded-xl border border-slate-200/80 bg-white/85 px-2 py-1.5 shadow-sm backdrop-blur">
|
||||
{#each breadcrumbItems as item, index}
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
<span class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-gradient-to-br from-slate-100 to-slate-200 text-slate-700 ring-1 ring-slate-200">
|
||||
<Icon name="layers" size={14} />
|
||||
</span>
|
||||
Menu
|
||||
{$t.nav?.menu || "Menu"}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-xs text-gray-500">M</span>
|
||||
@@ -285,7 +285,7 @@
|
||||
<span class="mr-2 inline-flex h-6 w-6 items-center justify-center rounded-md bg-slate-100 text-slate-600">
|
||||
<Icon name="chevronLeft" size={14} />
|
||||
</span>
|
||||
Collapse
|
||||
{$t.nav?.collapse || "Collapse"}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -293,10 +293,10 @@
|
||||
<button
|
||||
class="flex items-center justify-center w-full px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
on:click={handleToggleClick}
|
||||
aria-label="Expand sidebar"
|
||||
aria-label={$t.nav?.expand_sidebar || "Expand sidebar"}
|
||||
>
|
||||
<Icon name="chevronRight" size={16} />
|
||||
<span class="ml-2">Expand</span>
|
||||
<span class="ml-2">{$t.nav?.expand || "Expand"}</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
on:keydown={(e) => e.key === 'Escape' && handleClose()}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Close drawer"
|
||||
aria-label={$t.tasks?.close_drawer || "Close drawer"}
|
||||
>
|
||||
<!-- Drawer Panel -->
|
||||
<div
|
||||
@@ -209,7 +209,7 @@
|
||||
style={`right: ${assistantOffset};`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Task drawer"
|
||||
aria-label={$t.tasks?.drawer || "Task drawer"}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-slate-200 bg-white px-5 py-3.5">
|
||||
@@ -222,13 +222,13 @@
|
||||
<button
|
||||
class="flex items-center justify-center p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
|
||||
on:click={goBackToList}
|
||||
aria-label="Back to task list"
|
||||
aria-label={$t.tasks?.back_to_list || "Back to task list"}
|
||||
>
|
||||
<Icon name="back" size={16} strokeWidth={2} />
|
||||
</button>
|
||||
{/if}
|
||||
<h2 class="text-sm font-semibold tracking-tight text-slate-900">
|
||||
{activeTaskId ? ($t.tasks?.details_logs || 'Task Details & Logs') : 'Recent Tasks'}
|
||||
{activeTaskId ? ($t.tasks?.details_logs || 'Task Details & Logs') : ($t.tasks?.recent || 'Recent Tasks')}
|
||||
</h2>
|
||||
{#if shortTaskId}
|
||||
<span class="text-xs font-mono text-slate-500 bg-slate-800 px-2 py-0.5 rounded">{shortTaskId}…</span>
|
||||
@@ -249,7 +249,7 @@
|
||||
<button
|
||||
class="p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
|
||||
on:click={handleClose}
|
||||
aria-label="Close drawer"
|
||||
aria-label={$t.tasks?.close_drawer || "Close drawer"}
|
||||
>
|
||||
<Icon name="close" size={18} strokeWidth={2} />
|
||||
</button>
|
||||
@@ -268,19 +268,19 @@
|
||||
{:else if loadingTasks}
|
||||
<div class="flex flex-col items-center justify-center p-12 text-slate-500">
|
||||
<div class="mb-4 h-8 w-8 animate-spin rounded-full border-2 border-slate-200 border-t-blue-500"></div>
|
||||
<p>Loading tasks...</p>
|
||||
<p>{$t.tasks?.loading || 'Loading tasks...'}</p>
|
||||
</div>
|
||||
{:else if recentTasks.length > 0}
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold text-slate-100 mb-4 pb-2 border-b border-slate-800">Recent Tasks</h3>
|
||||
<h3 class="text-sm font-semibold text-slate-100 mb-4 pb-2 border-b border-slate-800">{$t.tasks?.recent || 'Recent Tasks'}</h3>
|
||||
{#each recentTasks as task}
|
||||
<button
|
||||
class="flex items-center gap-3 w-full p-3 mb-2 bg-slate-800 border border-slate-700 rounded-lg cursor-pointer transition-all hover:bg-slate-700 hover:border-slate-600 text-left"
|
||||
on:click={() => selectTask(task)}
|
||||
>
|
||||
<span class="font-mono text-xs text-slate-500">{task.id?.substring(0, 8) || 'N/A'}...</span>
|
||||
<span class="flex-1 text-sm text-slate-100 font-medium">{task.plugin_id || 'Unknown'}</span>
|
||||
<span class="text-xs font-semibold uppercase px-2 py-1 rounded-full {task.status?.toLowerCase() === 'running' || task.status?.toLowerCase() === 'pending' ? 'bg-cyan-500/15 text-cyan-400' : task.status?.toLowerCase() === 'completed' || task.status?.toLowerCase() === 'success' ? 'bg-green-500/15 text-green-400' : task.status?.toLowerCase() === 'failed' || task.status?.toLowerCase() === 'error' ? 'bg-red-500/15 text-red-400' : 'bg-slate-500/15 text-slate-400'}">{task.status || 'UNKNOWN'}</span>
|
||||
<span class="font-mono text-xs text-slate-500">{task.id?.substring(0, 8) || ($t.common?.not_available || 'N/A')}...</span>
|
||||
<span class="flex-1 text-sm text-slate-100 font-medium">{task.plugin_id || ($t.common?.unknown || 'Unknown')}</span>
|
||||
<span class="text-xs font-semibold uppercase px-2 py-1 rounded-full {task.status?.toLowerCase() === 'running' || task.status?.toLowerCase() === 'pending' ? 'bg-cyan-500/15 text-cyan-400' : task.status?.toLowerCase() === 'completed' || task.status?.toLowerCase() === 'success' ? 'bg-green-500/15 text-green-400' : task.status?.toLowerCase() === 'failed' || task.status?.toLowerCase() === 'error' ? 'bg-red-500/15 text-red-400' : 'bg-slate-500/15 text-slate-400'}">{task.status || ($t.common?.unknown || 'UNKNOWN')}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import { auth } from "$lib/auth/store.js";
|
||||
import { toggleAssistantChat } from "$lib/stores/assistantChat.js";
|
||||
import Icon from "$lib/ui/Icon.svelte";
|
||||
import LanguageSwitcher from "$lib/ui/LanguageSwitcher.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -105,7 +106,7 @@
|
||||
<button
|
||||
class="rounded-lg p-2 text-slate-600 transition-colors hover:bg-slate-100 md:hidden"
|
||||
on:click={handleHamburgerClick}
|
||||
aria-label="Toggle menu"
|
||||
aria-label={$t.common?.toggle_menu || "Toggle menu"}
|
||||
>
|
||||
<Icon name="menu" size={22} />
|
||||
</button>
|
||||
@@ -136,6 +137,8 @@
|
||||
|
||||
<!-- Nav Actions -->
|
||||
<div class="flex items-center gap-3 md:gap-4">
|
||||
<LanguageSwitcher />
|
||||
|
||||
<!-- Assistant -->
|
||||
<button
|
||||
class="rounded-lg p-2 text-slate-600 transition-colors hover:bg-slate-100"
|
||||
@@ -154,7 +157,7 @@
|
||||
(e.key === "Enter" || e.key === " ") && handleActivityClick()}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Activity"
|
||||
aria-label={$t.common?.activity || "Activity"}
|
||||
>
|
||||
<Icon name="activity" size={22} />
|
||||
{#if activeCount > 0}
|
||||
@@ -174,7 +177,7 @@
|
||||
(e.key === "Enter" || e.key === " ") && toggleUserMenu(e)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="User menu"
|
||||
aria-label={$t.common?.user_menu || "User menu"}
|
||||
>
|
||||
{#if user}
|
||||
<span
|
||||
@@ -192,7 +195,7 @@
|
||||
: 'hidden'}"
|
||||
>
|
||||
<div class="px-4 py-2 text-sm text-gray-700">
|
||||
<strong>{user?.username || "User"}</strong>
|
||||
<strong>{user?.username || ($t.common?.user || "User")}</strong>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"close": "Close",
|
||||
"back": "Back",
|
||||
"id": "ID",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
@@ -11,9 +14,25 @@
|
||||
"search": "Search...",
|
||||
"logout": "Logout",
|
||||
"refresh": "Refresh",
|
||||
"retry": "Retry"
|
||||
"retry": "Retry",
|
||||
"toggle_menu": "Toggle menu",
|
||||
"activity": "Activity",
|
||||
"user_menu": "User menu",
|
||||
"user": "User",
|
||||
"unknown": "Unknown",
|
||||
"not_available": "N/A",
|
||||
"first": "First",
|
||||
"last": "Last",
|
||||
"per_page": "per page",
|
||||
"close_modal": "Close modal"
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"breadcrumb_nav": "Breadcrumb navigation",
|
||||
"menu": "Menu",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"expand_sidebar": "Expand sidebar",
|
||||
"dashboard": "Dashboard",
|
||||
"dashboards": "Dashboards",
|
||||
"datasets": "Datasets",
|
||||
@@ -63,6 +82,8 @@
|
||||
"connection_success": "Connection successful!",
|
||||
"connection_failed": "Connection failed: {error}",
|
||||
"no_providers": "No providers configured.",
|
||||
"multimodal": "Multimodal",
|
||||
"text_only": "Text only",
|
||||
"doc_preview_title": "Documentation Preview",
|
||||
"dataset_desc": "Dataset Description",
|
||||
"column_doc": "Column Documentation",
|
||||
@@ -98,6 +119,17 @@
|
||||
"llm_prompt_documentation": "Documentation Prompt",
|
||||
"llm_prompt_dashboard_validation": "Dashboard Validation Prompt",
|
||||
"llm_prompt_git_commit": "Git Commit Prompt",
|
||||
"llm_chatbot_settings_title": "Chatbot Planner Settings",
|
||||
"llm_chatbot_settings_description": "Select provider and optional model override for assistant intent planning.",
|
||||
"llm_chatbot_provider": "Chatbot Provider",
|
||||
"llm_chatbot_model": "Chatbot Model Override",
|
||||
"llm_chatbot_model_placeholder": "Optional, e.g. gpt-4.1-mini",
|
||||
"llm_provider_bindings_title": "Provider Bindings by Task",
|
||||
"llm_provider_bindings_description": "Select which provider is used by default for each LLM task.",
|
||||
"llm_binding_dashboard_validation": "Dashboard Validation Provider",
|
||||
"llm_binding_documentation": "Documentation Provider",
|
||||
"llm_binding_git_commit": "Git Commit Provider",
|
||||
"llm_multimodal_warning": "Dashboard validation requires a multimodal model (image input support).",
|
||||
"save_llm_prompts": "Save LLM Prompts",
|
||||
"logging": "Logging Configuration",
|
||||
"logging_description": "Configure logging and task log levels.",
|
||||
@@ -125,7 +157,31 @@
|
||||
"server": "Git Server",
|
||||
"not_linked": "This dashboard is not yet linked to a Git repository.",
|
||||
"manage": "Manage Git",
|
||||
"generate_message": "Generate"
|
||||
"generate_message": "Generate",
|
||||
"select_dashboard": "Select Dashboard to Manage"
|
||||
,
|
||||
"commit_message_generated": "Commit message generated",
|
||||
"commit_message_failed": "Failed to generate message",
|
||||
"load_changes_failed": "Failed to load changes",
|
||||
"commit_success": "Changes committed successfully",
|
||||
"commit_message": "Commit Message",
|
||||
"generate_with_ai": "Generate with AI",
|
||||
"describe_changes": "Describe your changes...",
|
||||
"changed_files": "Changed Files",
|
||||
"changes_preview": "Changes Preview",
|
||||
"loading_diff": "Loading diff...",
|
||||
"no_changes": "No changes detected",
|
||||
"committing": "Committing...",
|
||||
"deploy_success": "Deployment triggered successfully",
|
||||
"no_deploy_envs": "No deployment environments configured.",
|
||||
"deploying": "Deploying...",
|
||||
"init_validation_error": "Please select a Git server and provide remote URL",
|
||||
"init_success": "Repository initialized successfully",
|
||||
"sync_success": "Dashboard state synced to Git",
|
||||
"push_success": "Changes pushed to remote",
|
||||
"pull_success": "Changes pulled from remote",
|
||||
"no_servers_configured": "No Git servers configured. Go to Settings -> Git to add one.",
|
||||
"remote_url_placeholder": "https://github.com/org/repo.git"
|
||||
},
|
||||
"dashboard": {
|
||||
"search": "Search dashboards...",
|
||||
@@ -199,6 +255,37 @@
|
||||
"last_task": "Last Task",
|
||||
"actions": "Actions",
|
||||
"action_map_columns": "Map Columns",
|
||||
"generate_docs": "Generate Docs",
|
||||
"generate_documentation": "Generate Documentation",
|
||||
"search_placeholder": "Search datasets...",
|
||||
"select_all": "Select All",
|
||||
"deselect_all": "Deselect All",
|
||||
"select_visible": "Select Visible",
|
||||
"deselect_visible": "Deselect Visible",
|
||||
"selected": "selected",
|
||||
"selected_count": "{count} selected",
|
||||
"bulk_map_columns": "Bulk Column Mapping",
|
||||
"bulk_docs_generation": "Bulk Documentation Generation",
|
||||
"source_type": "Source Type",
|
||||
"source_postgresql_comments": "PostgreSQL Comments",
|
||||
"source_xlsx": "XLSX File",
|
||||
"connection_id": "Connection ID",
|
||||
"connection_id_placeholder": "Enter connection ID...",
|
||||
"xlsx_file": "XLSX File",
|
||||
"selected_datasets": "Selected Datasets",
|
||||
"start_mapping": "Start Mapping",
|
||||
"select_llm_provider_option": "Select LLM provider...",
|
||||
"select_llm_provider": "Please select an LLM provider",
|
||||
"mapping_task_failed": "Failed to create mapping task",
|
||||
"docs_task_failed": "Failed to create documentation generation task",
|
||||
"load_failed": "Failed to load datasets",
|
||||
"load_detail_failed": "Failed to load dataset details",
|
||||
"missing_context": "Missing dataset ID or environment ID",
|
||||
"sql_lab_view": "SQL Lab View",
|
||||
"date_time": "Date/Time",
|
||||
"inactive": "Inactive",
|
||||
"mapped": "Mapped",
|
||||
"unmapped": "Unmapped",
|
||||
"view_task": "View task",
|
||||
"task_running": "Running...",
|
||||
"task_done": "Done",
|
||||
@@ -225,7 +312,30 @@
|
||||
"schedule_enabled": "Enabled",
|
||||
"cron_label": "Cron Expression",
|
||||
"cron_hint": "e.g., 0 0 * * * for daily at midnight",
|
||||
"footer_text": "Task continues running in background"
|
||||
"footer_text": "Task continues running in background",
|
||||
"drawer": "Task drawer",
|
||||
"close_drawer": "Close drawer",
|
||||
"back_to_list": "Back to task list"
|
||||
},
|
||||
"migration": {
|
||||
"source_env": "Source Environment",
|
||||
"target_env": "Target Environment",
|
||||
"loading_envs": "Loading environments...",
|
||||
"select_both_envs": "Please select both source and target environments.",
|
||||
"different_envs": "Source and target environments must be different.",
|
||||
"select_dashboards": "Please select at least one dashboard to migrate.",
|
||||
"select_dashboards_title": "Select Dashboards",
|
||||
"replace_db": "Replace Database (Apply Mappings)",
|
||||
"database_mappings": "Database Mappings",
|
||||
"loading_dbs": "Loading databases and suggestions...",
|
||||
"refresh_dbs": "Refresh Databases & Suggestions",
|
||||
"start": "Start Migration",
|
||||
"resume_failed": "Failed to resume task",
|
||||
"mapping_saved": "Mapping saved successfully",
|
||||
"mapping_management": "Database Mapping Management",
|
||||
"fetch_dbs": "Fetch Databases & Suggestions",
|
||||
"mapping_hint": "Select environments and click \"Fetch Databases\" to start mapping.",
|
||||
"task_placeholder_warn": "Could not fetch task details immediately, using placeholder."
|
||||
},
|
||||
"assistant": {
|
||||
"title": "AI Assistant",
|
||||
@@ -257,7 +367,14 @@
|
||||
"create": "Create Connection",
|
||||
"saved": "Saved Connections",
|
||||
"no_saved": "No connections saved yet.",
|
||||
"delete": "Delete"
|
||||
"delete": "Delete",
|
||||
"required_fields": "Please fill in all required fields",
|
||||
"created_success": "Connection created successfully",
|
||||
"fetch_failed": "Failed to fetch connections",
|
||||
"delete_confirm": "Are you sure you want to delete this connection?",
|
||||
"deleted_success": "Connection deleted",
|
||||
"name_placeholder": "e.g. Production DWH",
|
||||
"host_placeholder": "10.0.0.1"
|
||||
},
|
||||
"storage": {
|
||||
"management": "File Storage Management",
|
||||
@@ -307,16 +424,38 @@
|
||||
"excel_path": "Excel File Path",
|
||||
"run": "Run Mapper",
|
||||
"starting": "Starting...",
|
||||
"generating": "Generating...",
|
||||
"errors": {
|
||||
"fetch_failed": "Failed to fetch data",
|
||||
"required_fields": "Please fill in required fields",
|
||||
"postgres_required": "Connection and Table Name are required for postgres source",
|
||||
"excel_required": "Excel path is required for excel source"
|
||||
"excel_required": "Excel path is required for excel source",
|
||||
"no_active_llm_provider": "No active LLM provider found",
|
||||
"docs_start_failed": "Failed to start documentation generation",
|
||||
"docs_apply_failed": "Failed to apply documentation"
|
||||
},
|
||||
"success": {
|
||||
"started": "Mapper task started"
|
||||
"started": "Mapper task started",
|
||||
"docs_started": "Documentation generation started",
|
||||
"docs_applied": "Documentation applied successfully"
|
||||
},
|
||||
"auto_document": "Auto-Document"
|
||||
"auto_document": "Auto-Document",
|
||||
"excel_placeholder": "/path/to/mapping.xlsx"
|
||||
},
|
||||
"debug": {
|
||||
"title": "System Diagnostics",
|
||||
"action": "Debug Action",
|
||||
"test_db_api": "Test Database API (Compare Envs)",
|
||||
"get_dataset_structure": "Get Dataset Structure (JSON)",
|
||||
"source_target_required": "Source and Target environments are required",
|
||||
"env_dataset_required": "Environment and Dataset ID are required",
|
||||
"fetch_env_failed": "Failed to fetch environments",
|
||||
"completed": "Debug task completed",
|
||||
"failed": "Debug task failed",
|
||||
"run": "Run Diagnostics",
|
||||
"output": "Debug Output",
|
||||
"select_source": "-- Select Source --",
|
||||
"select_target": "-- Select Target --"
|
||||
},
|
||||
"admin": {
|
||||
"users": {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"save": "Сохранить",
|
||||
"close": "Закрыть",
|
||||
"back": "Назад",
|
||||
"id": "ID",
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить",
|
||||
"edit": "Редактировать",
|
||||
@@ -11,9 +14,25 @@
|
||||
"search": "Поиск...",
|
||||
"logout": "Выйти",
|
||||
"refresh": "Обновить",
|
||||
"retry": "Повторить"
|
||||
"retry": "Повторить",
|
||||
"toggle_menu": "Переключить меню",
|
||||
"activity": "Активность",
|
||||
"user_menu": "Меню пользователя",
|
||||
"user": "Пользователь",
|
||||
"unknown": "Неизвестно",
|
||||
"not_available": "Н/Д",
|
||||
"first": "Первая",
|
||||
"last": "Последняя",
|
||||
"per_page": "на страницу",
|
||||
"close_modal": "Закрыть модальное окно"
|
||||
},
|
||||
"nav": {
|
||||
"home": "Главная",
|
||||
"breadcrumb_nav": "Навигационная цепочка",
|
||||
"menu": "Меню",
|
||||
"collapse": "Свернуть",
|
||||
"expand": "Развернуть",
|
||||
"expand_sidebar": "Развернуть боковую панель",
|
||||
"dashboard": "Панель управления",
|
||||
"dashboards": "Дашборды",
|
||||
"datasets": "Датасеты",
|
||||
@@ -63,6 +82,8 @@
|
||||
"connection_success": "Подключение успешно!",
|
||||
"connection_failed": "Ошибка подключения: {error}",
|
||||
"no_providers": "Провайдеры не настроены.",
|
||||
"multimodal": "Мультимодальная",
|
||||
"text_only": "Только текст",
|
||||
"doc_preview_title": "Предпросмотр документации",
|
||||
"dataset_desc": "Описание датасета",
|
||||
"column_doc": "Документация колонок",
|
||||
@@ -98,6 +119,17 @@
|
||||
"llm_prompt_documentation": "Промпт документации",
|
||||
"llm_prompt_dashboard_validation": "Промпт проверки дашборда",
|
||||
"llm_prompt_git_commit": "Промпт git-коммита",
|
||||
"llm_chatbot_settings_title": "Настройки планировщика чат-бота",
|
||||
"llm_chatbot_settings_description": "Выберите провайдера и опциональную модель для планирования интентов ассистента.",
|
||||
"llm_chatbot_provider": "Провайдер чат-бота",
|
||||
"llm_chatbot_model": "Переопределение модели чат-бота",
|
||||
"llm_chatbot_model_placeholder": "Опционально, например gpt-4.1-mini",
|
||||
"llm_provider_bindings_title": "Назначение провайдеров по задачам",
|
||||
"llm_provider_bindings_description": "Выберите провайдера по умолчанию для каждой LLM-задачи.",
|
||||
"llm_binding_dashboard_validation": "Провайдер проверки дашборда",
|
||||
"llm_binding_documentation": "Провайдер документации",
|
||||
"llm_binding_git_commit": "Провайдер git-коммита",
|
||||
"llm_multimodal_warning": "Для проверки дашборда нужна мультимодальная модель (поддержка изображений).",
|
||||
"save_llm_prompts": "Сохранить промпты LLM",
|
||||
"logging": "Настройка логирования",
|
||||
"logging_description": "Настройка уровней логирования задач.",
|
||||
@@ -125,7 +157,31 @@
|
||||
"server": "Git-сервер",
|
||||
"not_linked": "Этот дашборд еще не привязан к Git-репозиторию.",
|
||||
"manage": "Управление Git",
|
||||
"generate_message": "Сгенерировать"
|
||||
"generate_message": "Сгенерировать",
|
||||
"select_dashboard": "Выберите дашборд для управления"
|
||||
,
|
||||
"commit_message_generated": "Сообщение коммита сгенерировано",
|
||||
"commit_message_failed": "Не удалось сгенерировать сообщение коммита",
|
||||
"load_changes_failed": "Не удалось загрузить изменения",
|
||||
"commit_success": "Изменения успешно закоммичены",
|
||||
"commit_message": "Сообщение коммита",
|
||||
"generate_with_ai": "Сгенерировать с AI",
|
||||
"describe_changes": "Опишите ваши изменения...",
|
||||
"changed_files": "Измененные файлы",
|
||||
"changes_preview": "Предпросмотр изменений",
|
||||
"loading_diff": "Загрузка diff...",
|
||||
"no_changes": "Изменения не обнаружены",
|
||||
"committing": "Коммит...",
|
||||
"deploy_success": "Деплой успешно запущен",
|
||||
"no_deploy_envs": "Окружения для деплоя не настроены.",
|
||||
"deploying": "Деплой...",
|
||||
"init_validation_error": "Выберите Git-сервер и укажите URL удаленного репозитория",
|
||||
"init_success": "Репозиторий успешно инициализирован",
|
||||
"sync_success": "Состояние дашборда синхронизировано с Git",
|
||||
"push_success": "Изменения отправлены в remote",
|
||||
"pull_success": "Изменения получены из remote",
|
||||
"no_servers_configured": "Git-серверы не настроены. Перейдите в Настройки -> Git для добавления.",
|
||||
"remote_url_placeholder": "https://github.com/org/repo.git"
|
||||
},
|
||||
"dashboard": {
|
||||
"search": "Поиск дашбордов...",
|
||||
@@ -198,6 +254,37 @@
|
||||
"last_task": "Последняя задача",
|
||||
"actions": "Действия",
|
||||
"action_map_columns": "Отобразить колонки",
|
||||
"generate_docs": "Сгенерировать документацию",
|
||||
"generate_documentation": "Сгенерировать документацию",
|
||||
"search_placeholder": "Поиск датасетов...",
|
||||
"select_all": "Выбрать все",
|
||||
"deselect_all": "Снять выбор со всех",
|
||||
"select_visible": "Выбрать видимые",
|
||||
"deselect_visible": "Снять выбор с видимых",
|
||||
"selected": "выбрано",
|
||||
"selected_count": "Выбрано: {count}",
|
||||
"bulk_map_columns": "Массовый маппинг колонок",
|
||||
"bulk_docs_generation": "Массовая генерация документации",
|
||||
"source_type": "Тип источника",
|
||||
"source_postgresql_comments": "Комментарии PostgreSQL",
|
||||
"source_xlsx": "XLSX-файл",
|
||||
"connection_id": "ID подключения",
|
||||
"connection_id_placeholder": "Введите ID подключения...",
|
||||
"xlsx_file": "XLSX-файл",
|
||||
"selected_datasets": "Выбранные датасеты",
|
||||
"start_mapping": "Запустить маппинг",
|
||||
"select_llm_provider_option": "Выберите LLM-провайдера...",
|
||||
"select_llm_provider": "Выберите LLM-провайдера",
|
||||
"mapping_task_failed": "Не удалось запустить задачу маппинга",
|
||||
"docs_task_failed": "Не удалось запустить задачу генерации документации",
|
||||
"load_failed": "Не удалось загрузить датасеты",
|
||||
"load_detail_failed": "Не удалось загрузить детали датасета",
|
||||
"missing_context": "Отсутствует ID датасета или окружения",
|
||||
"sql_lab_view": "Представление SQL Lab",
|
||||
"date_time": "Дата/время",
|
||||
"inactive": "Неактивно",
|
||||
"mapped": "Размечено",
|
||||
"unmapped": "Не размечено",
|
||||
"view_task": "Просмотреть задачу",
|
||||
"task_running": "Выполняется...",
|
||||
"task_done": "Готово",
|
||||
@@ -224,7 +311,30 @@
|
||||
"schedule_enabled": "Включено",
|
||||
"cron_label": "Cron-выражение",
|
||||
"cron_hint": "например, 0 0 * * * для ежедневного запуска в полночь",
|
||||
"footer_text": "Задача продолжает работать в фоновом режиме"
|
||||
"footer_text": "Задача продолжает работать в фоновом режиме",
|
||||
"drawer": "Панель задач",
|
||||
"close_drawer": "Закрыть панель задач",
|
||||
"back_to_list": "Назад к списку задач"
|
||||
},
|
||||
"migration": {
|
||||
"source_env": "Исходное окружение",
|
||||
"target_env": "Целевое окружение",
|
||||
"loading_envs": "Загрузка окружений...",
|
||||
"select_both_envs": "Выберите исходное и целевое окружение.",
|
||||
"different_envs": "Исходное и целевое окружения должны отличаться.",
|
||||
"select_dashboards": "Выберите хотя бы один дашборд для миграции.",
|
||||
"select_dashboards_title": "Выберите дашборды",
|
||||
"replace_db": "Заменить БД (применить маппинги)",
|
||||
"database_mappings": "Маппинги баз данных",
|
||||
"loading_dbs": "Загрузка баз данных и подсказок...",
|
||||
"refresh_dbs": "Обновить БД и подсказки",
|
||||
"start": "Запустить миграцию",
|
||||
"resume_failed": "Не удалось возобновить задачу",
|
||||
"mapping_saved": "Маппинг успешно сохранен",
|
||||
"mapping_management": "Управление маппингом БД",
|
||||
"fetch_dbs": "Получить БД и подсказки",
|
||||
"mapping_hint": "Выберите окружения и нажмите «Получить БД и подсказки», чтобы начать маппинг.",
|
||||
"task_placeholder_warn": "Не удалось сразу получить детали задачи, используется временное состояние."
|
||||
},
|
||||
"assistant": {
|
||||
"title": "AI Ассистент",
|
||||
@@ -256,7 +366,14 @@
|
||||
"create": "Создать подключение",
|
||||
"saved": "Сохраненные подключения",
|
||||
"no_saved": "Нет сохраненных подключений.",
|
||||
"delete": "Удалить"
|
||||
"delete": "Удалить",
|
||||
"required_fields": "Заполните все обязательные поля",
|
||||
"created_success": "Подключение успешно создано",
|
||||
"fetch_failed": "Не удалось загрузить подключения",
|
||||
"delete_confirm": "Вы уверены, что хотите удалить это подключение?",
|
||||
"deleted_success": "Подключение удалено",
|
||||
"name_placeholder": "например, Production DWH",
|
||||
"host_placeholder": "10.0.0.1"
|
||||
},
|
||||
"storage": {
|
||||
"management": "Управление хранилищем файлов",
|
||||
@@ -306,16 +423,38 @@
|
||||
"excel_path": "Путь к файлу Excel",
|
||||
"run": "Запустить маппер",
|
||||
"starting": "Запуск...",
|
||||
"generating": "Генерация...",
|
||||
"errors": {
|
||||
"fetch_failed": "Не удалось загрузить данные",
|
||||
"required_fields": "Пожалуйста, заполните обязательные поля",
|
||||
"postgres_required": "Подключение и имя таблицы обязательны для источника PostgreSQL",
|
||||
"excel_required": "Путь к Excel обязателен для источника Excel"
|
||||
"excel_required": "Путь к Excel обязателен для источника Excel",
|
||||
"no_active_llm_provider": "Не найден активный LLM-провайдер",
|
||||
"docs_start_failed": "Не удалось запустить генерацию документации",
|
||||
"docs_apply_failed": "Не удалось применить документацию"
|
||||
},
|
||||
"success": {
|
||||
"started": "Задача маппинга запущена"
|
||||
"started": "Задача маппинга запущена",
|
||||
"docs_started": "Генерация документации запущена",
|
||||
"docs_applied": "Документация успешно применена"
|
||||
},
|
||||
"auto_document": "Авто-документирование"
|
||||
"auto_document": "Авто-документирование",
|
||||
"excel_placeholder": "/path/to/mapping.xlsx"
|
||||
},
|
||||
"debug": {
|
||||
"title": "Системная диагностика",
|
||||
"action": "Действие отладки",
|
||||
"test_db_api": "Проверить Database API (сравнение окружений)",
|
||||
"get_dataset_structure": "Получить структуру датасета (JSON)",
|
||||
"source_target_required": "Требуются исходное и целевое окружения",
|
||||
"env_dataset_required": "Требуются окружение и ID датасета",
|
||||
"fetch_env_failed": "Не удалось загрузить окружения",
|
||||
"completed": "Задача диагностики завершена",
|
||||
"failed": "Задача диагностики завершилась с ошибкой",
|
||||
"run": "Запустить диагностику",
|
||||
"output": "Результат диагностики",
|
||||
"select_source": "-- Выберите источник --",
|
||||
"select_target": "-- Выберите цель --"
|
||||
},
|
||||
"admin": {
|
||||
"users": {
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
let providers = [];
|
||||
let loading = true;
|
||||
let savingPrompts = false;
|
||||
let plannerProvider = '';
|
||||
let plannerModel = '';
|
||||
let bindings = {
|
||||
dashboard_validation: '',
|
||||
documentation: '',
|
||||
git_commit: '',
|
||||
};
|
||||
let prompts = {
|
||||
documentation_prompt: '',
|
||||
dashboard_validation_prompt: '',
|
||||
@@ -31,6 +38,30 @@
|
||||
git_commit_prompt:
|
||||
"Generate a concise and professional git commit message based on the following diff and recent history.\\nUse Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\\n\\nRecent History:\\n{history}\\n\\nDiff:\\n{diff}\\n\\nCommit Message:",
|
||||
};
|
||||
const DEFAULT_LLM_PROVIDER_BINDINGS = {
|
||||
dashboard_validation: '',
|
||||
documentation: '',
|
||||
git_commit: '',
|
||||
};
|
||||
|
||||
function isMultimodalModel(modelName) {
|
||||
const token = (modelName || '').toLowerCase();
|
||||
if (!token) return false;
|
||||
return (
|
||||
token.includes('gpt-4o') ||
|
||||
token.includes('gpt-4.1') ||
|
||||
token.includes('vision') ||
|
||||
token.includes('vl') ||
|
||||
token.includes('gemini') ||
|
||||
token.includes('claude-3') ||
|
||||
token.includes('claude-sonnet-4')
|
||||
);
|
||||
}
|
||||
|
||||
function getProviderById(providerId) {
|
||||
if (!providerId) return null;
|
||||
return providers.find((item) => item.id === providerId) || null;
|
||||
}
|
||||
|
||||
async function fetchProviders() {
|
||||
loading = true;
|
||||
@@ -44,6 +75,12 @@
|
||||
...DEFAULT_LLM_PROMPTS,
|
||||
...(consolidatedSettings?.llm?.prompts || {}),
|
||||
};
|
||||
bindings = {
|
||||
...DEFAULT_LLM_PROVIDER_BINDINGS,
|
||||
...(consolidatedSettings?.llm?.provider_bindings || {}),
|
||||
};
|
||||
plannerProvider = consolidatedSettings?.llm?.assistant_planner_provider || '';
|
||||
plannerModel = consolidatedSettings?.llm?.assistant_planner_model || '';
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch providers", err);
|
||||
} finally {
|
||||
@@ -51,7 +88,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function savePrompts() {
|
||||
async function saveSettings() {
|
||||
savingPrompts = true;
|
||||
try {
|
||||
const current = await requestApi('/settings/consolidated');
|
||||
@@ -63,12 +100,18 @@
|
||||
...DEFAULT_LLM_PROMPTS,
|
||||
...prompts,
|
||||
},
|
||||
provider_bindings: {
|
||||
...DEFAULT_LLM_PROVIDER_BINDINGS,
|
||||
...bindings,
|
||||
},
|
||||
assistant_planner_provider: plannerProvider || '',
|
||||
assistant_planner_model: plannerModel || '',
|
||||
},
|
||||
};
|
||||
await requestApi('/settings/consolidated', 'PATCH', payload);
|
||||
addToast($t.settings?.save_success || 'Settings saved', 'success');
|
||||
} catch (err) {
|
||||
console.error('[LLMSettingsPage][Coherence:Failed] Failed to save prompts', err);
|
||||
console.error('[LLMSettingsPage][Coherence:Failed] Failed to save llm settings', err);
|
||||
addToast($t.settings?.save_failed || 'Failed to save settings', 'error');
|
||||
} finally {
|
||||
savingPrompts = false;
|
||||
@@ -93,6 +136,121 @@
|
||||
{:else}
|
||||
<ProviderConfig {providers} onSave={fetchProviders} />
|
||||
|
||||
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
{$t.settings?.llm_chatbot_settings_title || 'Chatbot Planner Settings'}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
{$t.settings?.llm_chatbot_settings_description ||
|
||||
'Select provider and optional model override for assistant intent planning.'}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="admin-planner-provider" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_chatbot_provider || 'Chatbot Provider'}
|
||||
</label>
|
||||
<select
|
||||
id="admin-planner-provider"
|
||||
bind:value={plannerProvider}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
>
|
||||
<option value="">{$t.dashboard?.use_default || 'Use Default'}</option>
|
||||
{#each providers as provider}
|
||||
<option value={provider.id}>
|
||||
{provider.name} ({provider.default_model})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="admin-planner-model" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_chatbot_model || 'Chatbot Model Override'}
|
||||
</label>
|
||||
<input
|
||||
id="admin-planner-model"
|
||||
type="text"
|
||||
bind:value={plannerModel}
|
||||
placeholder={$t.settings?.llm_chatbot_model_placeholder || 'Optional, e.g. gpt-4.1-mini'}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
{$t.settings?.llm_provider_bindings_title || 'Provider Bindings by Task'}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
{$t.settings?.llm_provider_bindings_description ||
|
||||
'Select which provider is used by default for each LLM task.'}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="admin-binding-dashboard-validation" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_binding_dashboard_validation || 'Dashboard Validation Provider'}
|
||||
</label>
|
||||
<select
|
||||
id="admin-binding-dashboard-validation"
|
||||
bind:value={bindings.dashboard_validation}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
>
|
||||
<option value="">{$t.dashboard?.use_default || 'Use Default'}</option>
|
||||
{#each providers as provider}
|
||||
<option value={provider.id}>
|
||||
{provider.name} ({provider.default_model})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if bindings.dashboard_validation && !isMultimodalModel(getProviderById(bindings.dashboard_validation)?.default_model)}
|
||||
<p class="mt-1 text-xs text-amber-700">
|
||||
{$t.settings?.llm_multimodal_warning ||
|
||||
'Dashboard validation requires a multimodal model (image input).'}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="admin-binding-documentation" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_binding_documentation || 'Documentation Provider'}
|
||||
</label>
|
||||
<select
|
||||
id="admin-binding-documentation"
|
||||
bind:value={bindings.documentation}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
>
|
||||
<option value="">{$t.dashboard?.use_default || 'Use Default'}</option>
|
||||
{#each providers as provider}
|
||||
<option value={provider.id}>
|
||||
{provider.name} ({provider.default_model})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<label for="admin-binding-git-commit" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_binding_git_commit || 'Git Commit Provider'}
|
||||
</label>
|
||||
<select
|
||||
id="admin-binding-git-commit"
|
||||
bind:value={bindings.git_commit}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
>
|
||||
<option value="">{$t.dashboard?.use_default || 'Use Default'}</option>
|
||||
{#each providers as provider}
|
||||
<option value={provider.id}>
|
||||
{provider.name} ({provider.default_model})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
{$t.settings?.llm_prompts_title || 'LLM Prompt Templates'}
|
||||
@@ -144,7 +302,7 @@
|
||||
<button
|
||||
class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-60"
|
||||
disabled={savingPrompts}
|
||||
on:click={savePrompts}
|
||||
on:click={saveSettings}
|
||||
>
|
||||
{savingPrompts ? '...' : ($t.settings?.save_llm_prompts || 'Save LLM Prompts')}
|
||||
</button>
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
// Update selection state
|
||||
updateSelectionState();
|
||||
} catch (err) {
|
||||
error = err.message || 'Failed to load datasets';
|
||||
error = err.message || ($t.datasets?.load_failed || 'Failed to load datasets');
|
||||
console.error('[DatasetHub][Coherence:Failed]', err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
@@ -289,7 +289,7 @@
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[DatasetHub][Coherence:Failed]', err);
|
||||
alert('Failed to create mapping task');
|
||||
alert($t.datasets?.mapping_task_failed || 'Failed to create mapping task');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
async function handleBulkGenerateDocs() {
|
||||
if (selectedIds.size === 0) return;
|
||||
if (!llmProvider) {
|
||||
alert('Please select an LLM provider');
|
||||
alert($t.datasets?.select_llm_provider || 'Please select an LLM provider');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[DatasetHub][Coherence:Failed]', err);
|
||||
alert('Failed to create documentation generation task');
|
||||
alert($t.datasets?.docs_task_failed || 'Failed to create documentation generation task');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,18 +430,18 @@
|
||||
on:click={handleSelectAll}
|
||||
disabled={total === 0}
|
||||
>
|
||||
{isAllSelected ? 'Deselect All' : 'Select All'}
|
||||
{isAllSelected ? ($t.datasets?.deselect_all || 'Deselect All') : ($t.datasets?.select_all || 'Select All')}
|
||||
</button>
|
||||
<button
|
||||
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100"
|
||||
on:click={handleSelectVisible}
|
||||
disabled={datasets.length === 0}
|
||||
>
|
||||
{isAllVisibleSelected ? 'Deselect Visible' : 'Select Visible'}
|
||||
{isAllVisibleSelected ? ($t.datasets?.deselect_visible || 'Deselect Visible') : ($t.datasets?.select_visible || 'Select Visible')}
|
||||
</button>
|
||||
{#if selectedIds.size > 0}
|
||||
<span class="text-sm text-gray-600">
|
||||
{selectedIds.size} selected
|
||||
{($t.datasets?.selected_count || "{count} selected").replace("{count}", String(selectedIds.size))}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -449,7 +449,7 @@
|
||||
<input
|
||||
type="text"
|
||||
class="px-2 py-1 border border-gray-300 rounded bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Search datasets..."
|
||||
placeholder={$t.datasets?.search_placeholder || "Search datasets..."}
|
||||
on:input={handleSearch}
|
||||
value={searchQuery}
|
||||
/>
|
||||
@@ -554,7 +554,10 @@
|
||||
{#if totalPages > 1}
|
||||
<div class="flex items-center justify-between px-3 py-3 bg-gray-50 border-t border-gray-200">
|
||||
<div class="text-sm text-gray-500">
|
||||
Showing {((currentPage - 1) * pageSize) + 1}-{Math.min(currentPage * pageSize, total)} of {total}
|
||||
{($t.dashboard?.showing || "Showing {start} to {end} of {total} dashboards")
|
||||
.replace("{start}", String(((currentPage - 1) * pageSize) + 1))
|
||||
.replace("{end}", String(Math.min(currentPage * pageSize, total)))
|
||||
.replace("{total}", String(total))}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@@ -562,14 +565,14 @@
|
||||
on:click={() => handlePageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
First
|
||||
{$t.common?.first || 'First'}
|
||||
</button>
|
||||
<button
|
||||
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
on:click={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
{$t.dashboard?.previous || 'Previous'}
|
||||
</button>
|
||||
{#each Array.from({length: totalPages}, (_, i) => i + 1) as pageNum}
|
||||
<button
|
||||
@@ -584,14 +587,14 @@
|
||||
on:click={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
{$t.dashboard?.next || 'Next'}
|
||||
</button>
|
||||
<button
|
||||
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
on:click={() => handlePageChange(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Last
|
||||
{$t.common?.last || 'Last'}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
@@ -600,11 +603,11 @@
|
||||
value={pageSize}
|
||||
on:change={handlePageSizeChange}
|
||||
>
|
||||
<option value={5}>5 per page</option>
|
||||
<option value={10}>10 per page</option>
|
||||
<option value={25}>25 per page</option>
|
||||
<option value={50}>50 per page</option>
|
||||
<option value={100}>100 per page</option>
|
||||
<option value={5}>5 {$t.common?.per_page || 'per page'}</option>
|
||||
<option value={10}>10 {$t.common?.per_page || 'per page'}</option>
|
||||
<option value={25}>25 {$t.common?.per_page || 'per page'}</option>
|
||||
<option value={50}>50 {$t.common?.per_page || 'per page'}</option>
|
||||
<option value={100}>100 {$t.common?.per_page || 'per page'}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -616,7 +619,7 @@
|
||||
<div class="flex items-center justify-between max-w-7xl mx-auto">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="font-medium">
|
||||
✓ {selectedIds.size} selected
|
||||
✓ {selectedIds.size} {$t.datasets?.selected || 'selected'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
@@ -624,19 +627,19 @@
|
||||
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
on:click={() => showMapColumnsModal = true}
|
||||
>
|
||||
Map Columns
|
||||
{$t.datasets?.action_map_columns || 'Map Columns'}
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
on:click={() => showGenerateDocsModal = true}
|
||||
>
|
||||
Generate Docs
|
||||
{$t.datasets?.generate_docs || 'Generate Docs'}
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100"
|
||||
on:click={() => selectedIds.clear()}
|
||||
>
|
||||
Cancel
|
||||
{$t.common?.cancel || 'Cancel'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -649,8 +652,8 @@
|
||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" on:click={() => showMapColumnsModal = false}>
|
||||
<div class="bg-white rounded-lg shadow-2xl max-w-xl w-full m-4 max-h-[80vh] overflow-y-auto" on:click|stopPropagation>
|
||||
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between relative">
|
||||
<h2 class="text-xl font-bold">Bulk Column Mapping</h2>
|
||||
<button on:click={() => showMapColumnsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label="Close modal">
|
||||
<h2 class="text-xl font-bold">{$t.datasets?.bulk_map_columns || 'Bulk Column Mapping'}</h2>
|
||||
<button on:click={() => showMapColumnsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label={$t.common?.close_modal || "Close modal"}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
@@ -660,28 +663,28 @@
|
||||
<div class="p-4">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Source Type</label>
|
||||
<label class="block text-sm font-medium mb-2">{$t.datasets?.source_type || 'Source Type'}</label>
|
||||
<select
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
bind:value={mapSourceType}
|
||||
>
|
||||
<option value="postgresql">PostgreSQL Comments</option>
|
||||
<option value="xlsx">XLSX File</option>
|
||||
<option value="postgresql">{$t.datasets?.source_postgresql_comments || 'PostgreSQL Comments'}</option>
|
||||
<option value="xlsx">{$t.datasets?.source_xlsx || 'XLSX File'}</option>
|
||||
</select>
|
||||
</div>
|
||||
{#if mapSourceType === 'postgresql'}
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Connection ID</label>
|
||||
<label class="block text-sm font-medium mb-2">{$t.datasets?.connection_id || 'Connection ID'}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter connection ID..."
|
||||
placeholder={$t.datasets?.connection_id_placeholder || "Enter connection ID..."}
|
||||
bind:value={mapConnectionId}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">XLSX File</label>
|
||||
<label class="block text-sm font-medium mb-2">{$t.datasets?.xlsx_file || 'XLSX File'}</label>
|
||||
<input
|
||||
type="file"
|
||||
class="w-full"
|
||||
@@ -692,7 +695,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Selected Datasets</label>
|
||||
<label class="block text-sm font-medium mb-2">{$t.datasets?.selected_datasets || 'Selected Datasets'}</label>
|
||||
<div class="max-h-40 overflow-y-auto">
|
||||
{#each Array.from(selectedIds) as id}
|
||||
{#each datasets as d}
|
||||
@@ -706,14 +709,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 py-3 border-t border-gray-200 flex justify-end gap-3">
|
||||
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showMapColumnsModal = false}>Cancel</button>
|
||||
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showMapColumnsModal = false}>{$t.common?.cancel || 'Cancel'}</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
on:click|preventDefault={handleBulkMapColumns}
|
||||
disabled={selectedIds.size === 0 || (mapSourceType === 'postgresql' && !mapConnectionId) || (mapSourceType === 'xlsx' && (!mapFileData || mapFileData.length === 0))}
|
||||
>
|
||||
Start Mapping
|
||||
{$t.datasets?.start_mapping || 'Start Mapping'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -725,8 +728,8 @@
|
||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" on:click={() => showGenerateDocsModal = false}>
|
||||
<div class="bg-white rounded-lg shadow-2xl max-w-xl w-full m-4 max-h-[80vh] overflow-y-auto" on:click|stopPropagation>
|
||||
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between relative">
|
||||
<h2 class="text-xl font-bold">Bulk Documentation Generation</h2>
|
||||
<button on:click={() => showGenerateDocsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label="Close modal">
|
||||
<h2 class="text-xl font-bold">{$t.datasets?.bulk_docs_generation || 'Bulk Documentation Generation'}</h2>
|
||||
<button on:click={() => showGenerateDocsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label={$t.common?.close_modal || "Close modal"}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
@@ -736,19 +739,19 @@
|
||||
<div class="p-4">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">LLM Provider</label>
|
||||
<label class="block text-sm font-medium mb-2">{$t.dashboard?.llm_provider || 'LLM Provider'}</label>
|
||||
<select
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
bind:value={llmProvider}
|
||||
>
|
||||
<option value="">Select LLM provider...</option>
|
||||
<option value="">{$t.datasets?.select_llm_provider_option || 'Select LLM provider...'}</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Selected Datasets</label>
|
||||
<label class="block text-sm font-medium mb-2">{$t.datasets?.selected_datasets || 'Selected Datasets'}</label>
|
||||
<div class="max-h-40 overflow-y-auto">
|
||||
{#each Array.from(selectedIds) as id}
|
||||
{#each datasets as d}
|
||||
@@ -762,13 +765,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 py-3 border-t border-gray-200 flex justify-end gap-3">
|
||||
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showGenerateDocsModal = false}>Cancel</button>
|
||||
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showGenerateDocsModal = false}>{$t.common?.cancel || 'Cancel'}</button>
|
||||
<button
|
||||
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
on:click={handleBulkGenerateDocs}
|
||||
disabled={!llmProvider || selectedIds.size === 0}
|
||||
>
|
||||
Generate Documentation
|
||||
{$t.datasets?.generate_documentation || 'Generate Documentation'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
// Load dataset details from API
|
||||
async function loadDatasetDetail() {
|
||||
if (!datasetId || !envId) {
|
||||
error = "Missing dataset ID or environment ID";
|
||||
error = $t.datasets?.missing_context || "Missing dataset ID or environment ID";
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
@@ -49,7 +49,7 @@
|
||||
const response = await api.getDatasetDetail(envId, datasetId);
|
||||
dataset = response;
|
||||
} catch (err) {
|
||||
error = err.message || "Failed to load dataset details";
|
||||
error = err.message || ($t.datasets?.load_detail_failed || "Failed to load dataset details");
|
||||
console.error("[DatasetDetail][Coherence:Failed]", err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
// Navigate back to dataset list
|
||||
function goBack() {
|
||||
goto(`/dashboards?env_id=${envId}`);
|
||||
goto(`/datasets?env_id=${envId}`);
|
||||
}
|
||||
|
||||
// Get column type icon/color
|
||||
@@ -232,7 +232,7 @@
|
||||
<span class="text-sm text-gray-500"
|
||||
>{$t.datasets?.type || "Type"}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-900">SQL Lab View</span>
|
||||
<span class="text-sm font-medium text-gray-900">{$t.datasets?.sql_lab_view || "SQL Lab View"}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if dataset.created_on}
|
||||
@@ -334,17 +334,17 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
{#if column.is_dttm}
|
||||
<span class="text-xs text-green-600">📅 Date/Time</span>
|
||||
<span class="text-xs text-green-600">📅 {$t.datasets?.date_time || "Date/Time"}</span>
|
||||
{/if}
|
||||
{#if !column.is_active}
|
||||
<span class="text-xs text-gray-400">(Inactive)</span>
|
||||
<span class="text-xs text-gray-400">({$t.datasets?.inactive || "Inactive"})</span>
|
||||
{/if}
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 text-xs rounded-full {column.description
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-600'}"
|
||||
>
|
||||
{column.description ? "✓ Mapped" : "Unmapped"}
|
||||
{column.description ? `✓ ${$t.datasets?.mapped || "Mapped"}` : ($t.datasets?.unmapped || "Unmapped")}
|
||||
</span>
|
||||
</div>
|
||||
{#if column.description}
|
||||
|
||||
@@ -64,10 +64,10 @@
|
||||
</script>
|
||||
|
||||
<div class="max-w-6xl mx-auto p-6">
|
||||
<PageHeader title="Git Dashboard Management">
|
||||
<PageHeader title={$t.git?.management || "Git Management"}>
|
||||
<div slot="actions" class="flex items-center space-x-4">
|
||||
<Select
|
||||
label="Environment"
|
||||
label={$t.dashboard?.environment || "Environment"}
|
||||
bind:value={selectedEnvId}
|
||||
options={environments.map(e => ({ value: e.id, label: e.name }))}
|
||||
/>
|
||||
@@ -79,13 +79,13 @@
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<Card title="Select Dashboard to Manage">
|
||||
<Card title={$t.git?.select_dashboard || "Select Dashboard to Manage"}>
|
||||
{#if fetchingDashboards}
|
||||
<p class="text-gray-500">Loading dashboards...</p>
|
||||
<p class="text-gray-500">{$t.common?.loading || "Loading..."}</p>
|
||||
{:else if dashboards.length > 0}
|
||||
<DashboardGrid {dashboards} />
|
||||
{:else}
|
||||
<p class="text-gray-500 italic">No dashboards found in this environment.</p>
|
||||
<p class="text-gray-500 italic">{$t.dashboard?.no_dashboards || "No dashboards found in this environment."}</p>
|
||||
{/if}
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
// Task status update will be handled by store/websocket
|
||||
} catch (e) {
|
||||
console.error("Failed to resume task:", e);
|
||||
passwordPromptErrorMessage = e.message;
|
||||
passwordPromptErrorMessage = e.message || ($t.migration?.resume_failed || "Failed to resume task");
|
||||
// Keep prompt open
|
||||
}
|
||||
}
|
||||
@@ -216,15 +216,15 @@
|
||||
*/
|
||||
async function startMigration() {
|
||||
if (!sourceEnvId || !targetEnvId) {
|
||||
error = "Please select both source and target environments.";
|
||||
error = $t.migration?.select_both_envs || "Please select both source and target environments.";
|
||||
return;
|
||||
}
|
||||
if (sourceEnvId === targetEnvId) {
|
||||
error = "Source and target environments must be different.";
|
||||
error = $t.migration?.different_envs || "Source and target environments must be different.";
|
||||
return;
|
||||
}
|
||||
if (selectedDashboardIds.length === 0) {
|
||||
error = "Please select at least one dashboard to migrate.";
|
||||
error = $t.migration?.select_dashboards || "Please select at least one dashboard to migrate.";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
selectedTask.set(task);
|
||||
} catch (fetchErr) {
|
||||
// Fallback: create a temporary task object to switch view immediately
|
||||
console.warn("Could not fetch task details immediately, using placeholder.");
|
||||
console.warn($t.migration?.task_placeholder_warn || "Could not fetch task details immediately, using placeholder.");
|
||||
selectedTask.set({
|
||||
id: result.task_id,
|
||||
plugin_id: 'superset-migration',
|
||||
@@ -283,7 +283,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
{#if loading}
|
||||
<p>Loading environments...</p>
|
||||
<p>{$t.migration?.loading_envs || "Loading environments..."}</p>
|
||||
{:else if error}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{error}
|
||||
@@ -292,12 +292,12 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<EnvSelector
|
||||
label="Source Environment"
|
||||
label={$t.migration?.source_env || "Source Environment"}
|
||||
bind:selectedId={sourceEnvId}
|
||||
{environments}
|
||||
/>
|
||||
<EnvSelector
|
||||
label="Target Environment"
|
||||
label={$t.migration?.target_env || "Target Environment"}
|
||||
bind:selectedId={targetEnvId}
|
||||
{environments}
|
||||
/>
|
||||
@@ -305,7 +305,7 @@
|
||||
|
||||
<!-- [DEF:DashboardSelectionSection:Component] -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-medium mb-4">Select Dashboards</h2>
|
||||
<h2 class="text-lg font-medium mb-4">{$t.migration?.select_dashboards_title || "Select Dashboards"}</h2>
|
||||
|
||||
{#if sourceEnvId}
|
||||
<DashboardGrid
|
||||
@@ -314,7 +314,7 @@
|
||||
environmentId={sourceEnvId}
|
||||
/>
|
||||
{:else}
|
||||
<p class="text-gray-500 italic">Select a source environment to view dashboards.</p>
|
||||
<p class="text-gray-500 italic">{$t.dashboard?.select_source || "Select a source environment to view dashboards."}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- [/DEF:DashboardSelectionSection:Component] -->
|
||||
@@ -329,15 +329,15 @@
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="replace-db" class="ml-2 block text-sm text-gray-900">
|
||||
Replace Database (Apply Mappings)
|
||||
{$t.migration?.replace_db || "Replace Database (Apply Mappings)"}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if replaceDb}
|
||||
<div class="mb-8 p-4 border rounded-md bg-gray-50">
|
||||
<h3 class="text-md font-medium mb-4">Database Mappings</h3>
|
||||
<h3 class="text-md font-medium mb-4">{$t.migration?.database_mappings || "Database Mappings"}</h3>
|
||||
{#if fetchingDbs}
|
||||
<p>Loading databases and suggestions...</p>
|
||||
<p>{$t.migration?.loading_dbs || "Loading databases and suggestions..."}</p>
|
||||
{:else if sourceDatabases.length > 0}
|
||||
<MappingTable
|
||||
{sourceDatabases}
|
||||
@@ -351,7 +351,7 @@
|
||||
on:click={fetchDatabases}
|
||||
class="text-indigo-600 hover:text-indigo-500 text-sm font-medium"
|
||||
>
|
||||
Refresh Databases & Suggestions
|
||||
{$t.migration?.refresh_dbs || "Refresh Databases & Suggestions"}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -361,7 +361,7 @@
|
||||
on:click={startMigration}
|
||||
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || selectedDashboardIds.length === 0}
|
||||
>
|
||||
Start Migration
|
||||
{$t.migration?.start || "Start Migration"}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
});
|
||||
|
||||
mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];
|
||||
success = "Mapping saved successfully";
|
||||
success = $t.migration?.mapping_saved || "Mapping saved successfully";
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
@@ -116,20 +116,20 @@
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="max-w-6xl mx-auto p-6">
|
||||
<PageHeader title="Database Mapping Management" />
|
||||
<PageHeader title={$t.migration?.mapping_management || "Database Mapping Management"} />
|
||||
|
||||
{#if loading}
|
||||
<p>Loading environments...</p>
|
||||
<p>{$t.migration?.loading_envs || "Loading environments..."}</p>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<EnvSelector
|
||||
label="Source Environment"
|
||||
label={$t.migration?.source_env || "Source Environment"}
|
||||
bind:selectedId={sourceEnvId}
|
||||
{environments}
|
||||
on:change={() => { sourceDatabases = []; mappings = []; suggestions = []; }}
|
||||
/>
|
||||
<EnvSelector
|
||||
label="Target Environment"
|
||||
label={$t.migration?.target_env || "Target Environment"}
|
||||
bind:selectedId={targetEnvId}
|
||||
{environments}
|
||||
on:change={() => { targetDatabases = []; mappings = []; suggestions = []; }}
|
||||
@@ -142,7 +142,7 @@
|
||||
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || fetchingDbs}
|
||||
isLoading={fetchingDbs}
|
||||
>
|
||||
Fetch Databases & Suggestions
|
||||
{$t.migration?.fetch_dbs || "Fetch Databases & Suggestions"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
on:update={handleUpdate}
|
||||
/>
|
||||
{:else if !fetchingDbs && sourceEnvId && targetEnvId}
|
||||
<p class="text-gray-500 italic">Select environments and click "Fetch Databases" to start mapping.</p>
|
||||
<p class="text-gray-500 italic">{$t.migration?.mapping_hint || 'Select environments and click "Fetch Databases" to start mapping.'}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
git_commit_prompt:
|
||||
"Generate a concise and professional git commit message based on the following diff and recent history.\\nUse Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\\n\\nRecent History:\\n{history}\\n\\nDiff:\\n{diff}\\n\\nCommit Message:",
|
||||
};
|
||||
const DEFAULT_LLM_PROVIDER_BINDINGS = {
|
||||
dashboard_validation: "",
|
||||
documentation: "",
|
||||
git_commit: "",
|
||||
};
|
||||
|
||||
// State
|
||||
let activeTab = "environments";
|
||||
@@ -77,15 +82,50 @@
|
||||
providers: [],
|
||||
default_provider: "",
|
||||
prompts: { ...DEFAULT_LLM_PROMPTS },
|
||||
provider_bindings: { ...DEFAULT_LLM_PROVIDER_BINDINGS },
|
||||
assistant_planner_provider: "",
|
||||
assistant_planner_model: "",
|
||||
...(llm || {}),
|
||||
};
|
||||
normalized.prompts = {
|
||||
...DEFAULT_LLM_PROMPTS,
|
||||
...(llm?.prompts || {}),
|
||||
};
|
||||
normalized.provider_bindings = {
|
||||
...DEFAULT_LLM_PROVIDER_BINDINGS,
|
||||
...(llm?.provider_bindings || {}),
|
||||
};
|
||||
normalized.assistant_planner_provider = llm?.assistant_planner_provider || "";
|
||||
normalized.assistant_planner_model = llm?.assistant_planner_model || "";
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function isMultimodalModel(modelName) {
|
||||
const token = (modelName || "").toLowerCase();
|
||||
if (!token) return false;
|
||||
return (
|
||||
token.includes("gpt-4o") ||
|
||||
token.includes("gpt-4.1") ||
|
||||
token.includes("vision") ||
|
||||
token.includes("vl") ||
|
||||
token.includes("gemini") ||
|
||||
token.includes("claude-3") ||
|
||||
token.includes("claude-sonnet-4")
|
||||
);
|
||||
}
|
||||
|
||||
function getProviderById(providerId) {
|
||||
if (!providerId) return null;
|
||||
return (settings?.llm_providers || []).find((p) => p.id === providerId) || null;
|
||||
}
|
||||
|
||||
function isDashboardValidationBindingValid() {
|
||||
const providerId = settings?.llm?.provider_bindings?.dashboard_validation;
|
||||
if (!providerId) return true;
|
||||
const provider = getProviderById(providerId);
|
||||
return provider ? isMultimodalModel(provider.default_model) : true;
|
||||
}
|
||||
|
||||
// Handle tab change
|
||||
function handleTabChange(tab) {
|
||||
activeTab = tab;
|
||||
@@ -670,6 +710,121 @@
|
||||
onSave={loadSettings}
|
||||
/>
|
||||
|
||||
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h3 class="text-base font-semibold text-gray-900">
|
||||
{$t.settings?.llm_chatbot_settings_title || "Chatbot Planner Settings"}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
{$t.settings?.llm_chatbot_settings_description ||
|
||||
"Select provider and optional model override for assistant intent planning."}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="planner-provider" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_chatbot_provider || "Chatbot Provider"}
|
||||
</label>
|
||||
<select
|
||||
id="planner-provider"
|
||||
bind:value={settings.llm.assistant_planner_provider}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
>
|
||||
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
|
||||
{#each settings.llm_providers || [] as provider}
|
||||
<option value={provider.id}>
|
||||
{provider.name} ({provider.default_model})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="planner-model" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_chatbot_model || "Chatbot Model Override"}
|
||||
</label>
|
||||
<input
|
||||
id="planner-model"
|
||||
type="text"
|
||||
bind:value={settings.llm.assistant_planner_model}
|
||||
placeholder={$t.settings?.llm_chatbot_model_placeholder || "Optional, e.g. gpt-4.1-mini"}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h3 class="text-base font-semibold text-gray-900">
|
||||
{$t.settings?.llm_provider_bindings_title || "Provider Bindings by Task"}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
{$t.settings?.llm_provider_bindings_description ||
|
||||
"Select which provider is used by default for each LLM task."}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="binding-dashboard-validation" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_binding_dashboard_validation || "Dashboard Validation Provider"}
|
||||
</label>
|
||||
<select
|
||||
id="binding-dashboard-validation"
|
||||
bind:value={settings.llm.provider_bindings.dashboard_validation}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
>
|
||||
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
|
||||
{#each settings.llm_providers || [] as provider}
|
||||
<option value={provider.id}>
|
||||
{provider.name} ({provider.default_model})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if !isDashboardValidationBindingValid()}
|
||||
<p class="mt-1 text-xs text-amber-700">
|
||||
{$t.settings?.llm_multimodal_warning ||
|
||||
"Dashboard validation requires a multimodal model (image input)."}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="binding-documentation" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_binding_documentation || "Documentation Provider"}
|
||||
</label>
|
||||
<select
|
||||
id="binding-documentation"
|
||||
bind:value={settings.llm.provider_bindings.documentation}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
>
|
||||
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
|
||||
{#each settings.llm_providers || [] as provider}
|
||||
<option value={provider.id}>
|
||||
{provider.name} ({provider.default_model})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<label for="binding-git-commit" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_binding_git_commit || "Git Commit Provider"}
|
||||
</label>
|
||||
<select
|
||||
id="binding-git-commit"
|
||||
bind:value={settings.llm.provider_bindings.git_commit}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||||
>
|
||||
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
|
||||
{#each settings.llm_providers || [] as provider}
|
||||
<option value={provider.id}>
|
||||
{provider.name} ({provider.default_model})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<h3 class="text-base font-semibold text-gray-900">
|
||||
{$t.settings?.llm_prompts_title || "LLM Prompt Templates"}
|
||||
|
||||
Reference in New Issue
Block a user