Autosave: 20260619-151237
This commit is contained in:
parent
984076ccc4
commit
65e988640d
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,43 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from .models import ActionPlanStep, ProblemCase, RootCause, SolutionOption
|
||||
|
||||
|
||||
class RootCauseInline(admin.TabularInline):
|
||||
model = RootCause
|
||||
extra = 0
|
||||
|
||||
|
||||
class SolutionOptionInline(admin.TabularInline):
|
||||
model = SolutionOption
|
||||
extra = 0
|
||||
fields = ("rank", "title", "impact", "efficiency", "speed", "low_risk", "decision_score", "success_rate")
|
||||
readonly_fields = ("decision_score",)
|
||||
|
||||
|
||||
@admin.register(ProblemCase)
|
||||
class ProblemCaseAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "business_area", "urgency", "priority_score", "financial_impact", "status", "created_at")
|
||||
list_filter = ("business_area", "status", "urgency")
|
||||
search_fields = ("title", "description")
|
||||
inlines = [RootCauseInline, SolutionOptionInline]
|
||||
|
||||
|
||||
@admin.register(SolutionOption)
|
||||
class SolutionOptionAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "problem", "rank", "decision_score", "success_rate")
|
||||
list_filter = ("rank",)
|
||||
search_fields = ("title", "problem__title")
|
||||
|
||||
|
||||
@admin.register(ActionPlanStep)
|
||||
class ActionPlanStepAdmin(admin.ModelAdmin):
|
||||
list_display = ("solution", "day_index", "title", "is_done")
|
||||
list_filter = ("is_done",)
|
||||
search_fields = ("title", "task", "solution__title")
|
||||
|
||||
|
||||
@admin.register(RootCause)
|
||||
class RootCauseAdmin(admin.ModelAdmin):
|
||||
list_display = ("factor", "problem", "contribution_score", "parent")
|
||||
search_fields = ("factor", "why_chain", "problem__title")
|
||||
|
||||
40
core/forms.py
Normal file
40
core/forms.py
Normal file
@ -0,0 +1,40 @@
|
||||
from django import forms
|
||||
|
||||
from .models import ProblemCase
|
||||
|
||||
|
||||
class ProblemCaseForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ProblemCase
|
||||
fields = ["description", "urgency"]
|
||||
labels = {
|
||||
"description": "Masukkan masalah, target, atau hambatan Anda:",
|
||||
"urgency": "Tingkat Urgensi",
|
||||
}
|
||||
widgets = {
|
||||
"description": forms.Textarea(attrs={
|
||||
"class": "form-control problem-textarea",
|
||||
"rows": 9,
|
||||
"placeholder": "Contoh: Kuliah di Singapura atau Jepang, dana 200 juta, ingin 3 tahun selesai.",
|
||||
}),
|
||||
"urgency": forms.TextInput(attrs={
|
||||
"type": "range",
|
||||
"class": "form-range urgency-slider",
|
||||
"min": 1,
|
||||
"max": 5,
|
||||
"step": 1,
|
||||
"oninput": "document.getElementById('urgency-output').value=this.value",
|
||||
}),
|
||||
}
|
||||
|
||||
def clean_description(self):
|
||||
description = self.cleaned_data["description"].strip()
|
||||
if len(description) < 20:
|
||||
raise forms.ValidationError("Tuliskan minimal 20 karakter agar analisis lebih bermakna.")
|
||||
return description
|
||||
|
||||
def clean_urgency(self):
|
||||
urgency = self.cleaned_data["urgency"]
|
||||
if urgency < 1 or urgency > 5:
|
||||
raise forms.ValidationError("Urgensi harus berada pada skala 1 sampai 5.")
|
||||
return urgency
|
||||
82
core/migrations/0001_initial.py
Normal file
82
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,82 @@
|
||||
# Generated by Django 5.2.7 on 2026-06-19 14:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProblemCase',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=160, verbose_name='judul kasus')),
|
||||
('description', models.TextField(verbose_name='deskripsi masalah')),
|
||||
('business_area', models.CharField(choices=[('sales', 'Penjualan'), ('operations', 'Operasional'), ('finance', 'Keuangan'), ('marketing', 'Marketing'), ('product', 'Produk/Layanan'), ('people', 'Tim & SDM'), ('other', 'Lainnya')], default='sales', max_length=32, verbose_name='area bisnis')),
|
||||
('urgency', models.PositiveSmallIntegerField(default=3, verbose_name='urgensi')),
|
||||
('priority_score', models.PositiveSmallIntegerField(default=0, verbose_name='skor prioritas')),
|
||||
('financial_impact', models.CharField(default='Sedang', max_length=32, verbose_name='dampak finansial')),
|
||||
('status', models.CharField(choices=[('draft', 'Draft'), ('analyzed', 'Sudah dianalisis')], default='draft', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Problem Case',
|
||||
'verbose_name_plural': 'Problem Cases',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RootCause',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('factor', models.CharField(max_length=120)),
|
||||
('contribution_score', models.PositiveSmallIntegerField(default=70)),
|
||||
('why_chain', models.TextField()),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='core.rootcause')),
|
||||
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='root_causes', to='core.problemcase')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-contribution_score', 'factor'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SolutionOption',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=160)),
|
||||
('impact', models.PositiveSmallIntegerField(default=70)),
|
||||
('efficiency', models.PositiveSmallIntegerField(default=70)),
|
||||
('speed', models.PositiveSmallIntegerField(default=70)),
|
||||
('low_risk', models.PositiveSmallIntegerField(default=70)),
|
||||
('decision_score', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
|
||||
('success_rate', models.PositiveSmallIntegerField(default=70)),
|
||||
('rank', models.PositiveSmallIntegerField(default=1)),
|
||||
('rationale', models.TextField()),
|
||||
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='core.problemcase')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['rank', '-decision_score'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ActionPlanStep',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('day_index', models.PositiveSmallIntegerField()),
|
||||
('title', models.CharField(max_length=120)),
|
||||
('task', models.TextField()),
|
||||
('is_done', models.BooleanField(default=False)),
|
||||
('solution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='action_steps', to='core.solutionoption')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['day_index'],
|
||||
},
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
106
core/models.py
106
core/models.py
@ -1,3 +1,107 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class ProblemCase(models.Model):
|
||||
AREA_SALES = "sales"
|
||||
AREA_OPERATIONS = "operations"
|
||||
AREA_FINANCE = "finance"
|
||||
AREA_MARKETING = "marketing"
|
||||
AREA_PRODUCT = "product"
|
||||
AREA_PEOPLE = "people"
|
||||
AREA_OTHER = "other"
|
||||
|
||||
BUSINESS_AREA_CHOICES = [
|
||||
(AREA_SALES, "Penjualan"),
|
||||
(AREA_OPERATIONS, "Operasional"),
|
||||
(AREA_FINANCE, "Keuangan"),
|
||||
(AREA_MARKETING, "Marketing"),
|
||||
(AREA_PRODUCT, "Produk/Layanan"),
|
||||
(AREA_PEOPLE, "Tim & SDM"),
|
||||
(AREA_OTHER, "Lainnya"),
|
||||
]
|
||||
|
||||
STATUS_DRAFT = "draft"
|
||||
STATUS_ANALYZED = "analyzed"
|
||||
STATUS_CHOICES = [
|
||||
(STATUS_DRAFT, "Draft"),
|
||||
(STATUS_ANALYZED, "Sudah dianalisis"),
|
||||
]
|
||||
|
||||
title = models.CharField("judul kasus", max_length=160)
|
||||
description = models.TextField("deskripsi masalah")
|
||||
business_area = models.CharField(
|
||||
"area bisnis", max_length=32, choices=BUSINESS_AREA_CHOICES, default=AREA_SALES
|
||||
)
|
||||
urgency = models.PositiveSmallIntegerField("urgensi", default=3)
|
||||
priority_score = models.PositiveSmallIntegerField("skor prioritas", default=0)
|
||||
financial_impact = models.CharField("dampak finansial", max_length=32, default="Sedang")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_DRAFT)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
verbose_name = "Problem Case"
|
||||
verbose_name_plural = "Problem Cases"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("case_detail", kwargs={"pk": self.pk})
|
||||
|
||||
|
||||
class RootCause(models.Model):
|
||||
problem = models.ForeignKey(
|
||||
ProblemCase, related_name="root_causes", on_delete=models.CASCADE
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
"self", related_name="children", null=True, blank=True, on_delete=models.CASCADE
|
||||
)
|
||||
factor = models.CharField(max_length=120)
|
||||
contribution_score = models.PositiveSmallIntegerField(default=70)
|
||||
why_chain = models.TextField()
|
||||
|
||||
class Meta:
|
||||
ordering = ["-contribution_score", "factor"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.factor} ({self.contribution_score}%)"
|
||||
|
||||
|
||||
class SolutionOption(models.Model):
|
||||
problem = models.ForeignKey(
|
||||
ProblemCase, related_name="solutions", on_delete=models.CASCADE
|
||||
)
|
||||
title = models.CharField(max_length=160)
|
||||
impact = models.PositiveSmallIntegerField(default=70)
|
||||
efficiency = models.PositiveSmallIntegerField(default=70)
|
||||
speed = models.PositiveSmallIntegerField(default=70)
|
||||
low_risk = models.PositiveSmallIntegerField(default=70)
|
||||
decision_score = models.DecimalField(max_digits=5, decimal_places=2, default=0)
|
||||
success_rate = models.PositiveSmallIntegerField(default=70)
|
||||
rank = models.PositiveSmallIntegerField(default=1)
|
||||
rationale = models.TextField()
|
||||
|
||||
class Meta:
|
||||
ordering = ["rank", "-decision_score"]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class ActionPlanStep(models.Model):
|
||||
solution = models.ForeignKey(
|
||||
SolutionOption, related_name="action_steps", on_delete=models.CASCADE
|
||||
)
|
||||
day_index = models.PositiveSmallIntegerField()
|
||||
title = models.CharField(max_length=120)
|
||||
task = models.TextField()
|
||||
is_done = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ["day_index"]
|
||||
|
||||
def __str__(self):
|
||||
return f"Hari {self.day_index}: {self.title}"
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="id">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}{{ page_title|default:"OPTEMA AI" }}{% endblock %}</title>
|
||||
<meta name="description" content="{{ meta_description|default:project_description|default:'OPTEMA AI mengubah masalah menjadi analisis akar masalah, skor keputusan, dan rencana aksi.' }}">
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
@ -13,13 +15,17 @@
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Plus+Jakarta+Sans:wght@600;700;800&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
17
core/templates/core/_nav.html
Normal file
17
core/templates/core/_nav.html
Normal file
@ -0,0 +1,17 @@
|
||||
<nav class="navbar navbar-expand-lg optema-nav" aria-label="Navigasi utama OPTEMA AI">
|
||||
<div class="container">
|
||||
<a class="navbar-brand brand-mark" href="{% url 'home' %}">
|
||||
<span class="brand-icon">O</span> OPTEMA AI
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#optemaNav" aria-controls="optemaNav" aria-expanded="false" aria-label="Buka navigasi">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="optemaNav">
|
||||
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'home' %}#analisis">Mulai Analisis</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'case_list' %}">Daftar Kasus</a></li>
|
||||
<li class="nav-item"><a class="nav-link admin-link" href="/admin/">Admin</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
124
core/templates/core/case_detail.html
Normal file
124
core/templates/core/case_detail.html
Normal file
@ -0,0 +1,124 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
{% include "core/_nav.html" %}
|
||||
<main class="page-shell">
|
||||
<section class="detail-hero">
|
||||
<div class="container">
|
||||
{% if messages %}
|
||||
<div class="mb-4">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags|default:'info' }}" role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<p class="eyebrow mb-2">Analisis Tersimpan · {{ problem.get_business_area_display }}</p>
|
||||
<h1 class="page-title">{{ problem.title }}</h1>
|
||||
<p class="hero-copy mb-0">{{ problem.description }}</p>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="detail-status-card glass-card">
|
||||
<div class="output-success mb-3">✅ Analisis Berhasil Disimulasikan!</div>
|
||||
<p class="mb-0">Case ini tersimpan di database lokal dan bisa dibuka ulang dari Riwayat Case.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container py-5">
|
||||
<div class="analysis-card mb-4">
|
||||
<div class="section-kicker">Problem Detection</div>
|
||||
<h2>1. Problem Detection</h2>
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card"><span>Prioritas Skor</span><strong>{{ problem.priority_score }}/100</strong></div>
|
||||
<div class="metric-card"><span>Dampak Finansial</span><strong>{{ problem.financial_impact }}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-5">
|
||||
<div class="analysis-card sticky-lg-top">
|
||||
<div class="section-kicker">Root Cause Analysis</div>
|
||||
<h2>2. Root Cause (Akar Masalah)</h2>
|
||||
<div class="cause-list">
|
||||
{% for cause in problem.root_causes.all %}
|
||||
<article>
|
||||
<div class="d-flex justify-content-between gap-3"><strong>{{ cause.factor }}</strong><span>{{ cause.contribution_score }}%</span></div>
|
||||
<div class="progress" role="progressbar" aria-label="Kontribusi {{ cause.factor }}" aria-valuenow="{{ cause.contribution_score }}" aria-valuemin="0" aria-valuemax="100">
|
||||
<div class="progress-bar score-width-{{ cause.contribution_score }}"></div>
|
||||
</div>
|
||||
<p>{{ cause.why_chain }}</p>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="analysis-card mb-4">
|
||||
<div class="section-kicker">Solution & Decision Scoring</div>
|
||||
<h2>3. Rekomendasi Solusi & Prediksi Sukses</h2>
|
||||
<p class="formula-note">Rumus: Decision Score = Impact×0.4 + Efficiency×0.3 + Speed×0.2 + LowRisk×0.1</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle solution-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Solusi</th>
|
||||
<th>Impact</th>
|
||||
<th>Efficiency</th>
|
||||
<th>Speed</th>
|
||||
<th>Low Risk</th>
|
||||
<th>Sukses Rate</th>
|
||||
<th>Decision Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for solution in problem.solutions.all %}
|
||||
<tr>
|
||||
<td><span class="rank-badge">#{{ solution.rank }}</span></td>
|
||||
<td>
|
||||
<strong>{{ solution.title }}</strong>
|
||||
<p>{{ solution.rationale }}</p>
|
||||
</td>
|
||||
<td>{{ solution.impact }}</td>
|
||||
<td>{{ solution.efficiency }}</td>
|
||||
<td>{{ solution.speed }}</td>
|
||||
<td>{{ solution.low_risk }}</td>
|
||||
<td>{{ solution.success_rate }}%</td>
|
||||
<td><strong>{{ solution.decision_score|floatformat:1 }}</strong></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if top_solution %}
|
||||
<div class="analysis-card action-card">
|
||||
<div class="section-kicker">Action Plan</div>
|
||||
<h2>4. Action Plan 5 Hari: {{ top_solution.title }}</h2>
|
||||
<div class="timeline">
|
||||
{% for step in top_solution.action_steps.all %}
|
||||
<article>
|
||||
<span>Hari {{ step.day_index }}</span>
|
||||
<div>
|
||||
<h3>{{ step.title }}</h3>
|
||||
<p>{{ step.task }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 d-flex gap-3 flex-wrap">
|
||||
<a href="{% url 'home' %}#analisis" class="btn btn-optema">Analisis Kasus Baru</a>
|
||||
<a href="{% url 'case_list' %}" class="btn btn-outline-optema">Kembali ke Riwayat Case</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
37
core/templates/core/case_list.html
Normal file
37
core/templates/core/case_list.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
{% include "core/_nav.html" %}
|
||||
<main class="page-shell">
|
||||
<section class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-end mb-4 flex-wrap gap-3">
|
||||
<div>
|
||||
<p class="eyebrow mb-2">Case Library</p>
|
||||
<h1 class="page-title">Daftar Kasus OPTEMA</h1>
|
||||
<p class="section-copy mb-0">Histori masalah yang sudah diproses menjadi keputusan dan action plan.</p>
|
||||
</div>
|
||||
<a href="{% url 'home' %}#analisis" class="btn btn-optema">Tambah Kasus Baru</a>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
{% for case in cases %}
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<a class="case-card h-100" href="{{ case.get_absolute_url }}">
|
||||
<span class="case-area">{{ case.get_business_area_display }} · Urgensi {{ case.urgency }}/5</span>
|
||||
<h2>{{ case.title }}</h2>
|
||||
<p>{{ case.description|truncatechars:130 }}</p>
|
||||
<div class="case-meta"><span>Prioritas {{ case.priority_score }}/100</span><span>{{ case.created_at|date:"d M Y" }}</span></div>
|
||||
</a>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="empty-state">
|
||||
<h2>Case library masih kosong.</h2>
|
||||
<p>Buat analisis pertama untuk mulai membangun histori keputusan.</p>
|
||||
<a href="{% url 'home' %}#analisis" class="btn btn-optema">Mulai Analisis</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@ -1,145 +1,201 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "core/_nav.html" %}
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<section class="hero-section mvp-hero position-relative overflow-hidden">
|
||||
<div class="shape shape-one" aria-hidden="true"></div>
|
||||
<div class="shape shape-two" aria-hidden="true"></div>
|
||||
<div class="container position-relative">
|
||||
<div class="row align-items-center g-5 py-5">
|
||||
<div class="col-lg-7">
|
||||
<p class="eyebrow mb-3">Mesin Pemecah Masalah Universal & Decision Intelligence Platform</p>
|
||||
<h1 class="display-heading mb-4">💡 OPTEMA AI</h1>
|
||||
<h2 class="hero-subtitle mb-3">Optimal, Effective, Efficient Management Assistant</h2>
|
||||
<p class="hero-copy mb-4">MVP single-user tanpa login: masukkan masalah bisnis, keuangan, karier, logistik, teknologi, atau pendidikan; atur urgensi, lalu dapatkan simulasi Problem Detection, Root Cause, Decision Scoring, dan Action Plan yang tersimpan otomatis.</p>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a href="#analisis" class="btn btn-optema btn-lg">Jalankan Analisis AI</a>
|
||||
<a href="{% url 'case_list' %}" class="btn btn-outline-optema btn-lg">Lihat Riwayat Case</a>
|
||||
</div>
|
||||
<div class="trust-strip mt-4" aria-label="Modul MVP">
|
||||
<span>Problem Detection</span>
|
||||
<span>Root Cause</span>
|
||||
<span>Decision Score</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="decision-panel glass-card">
|
||||
<div class="panel-topline d-flex justify-content-between align-items-center mb-4">
|
||||
<span class="status-dot"><i></i> Simulasi MVP</span>
|
||||
<span class="score-pill">Prioritas 95/100</span>
|
||||
</div>
|
||||
<div class="mini-chart mb-4" aria-label="Preview root cause score">
|
||||
<div class="bar-95"><span>Keterbatasan Dana</span><b>96%</b></div>
|
||||
<div class="bar-88"><span>Target 3 Tahun</span><b>92%</b></div>
|
||||
<div class="bar-85"><span>Biaya Hidup</span><b>88%</b></div>
|
||||
<div class="bar-70"><span>Bahasa & Admission</span><b>76%</b></div>
|
||||
</div>
|
||||
<div class="solution-preview">
|
||||
<p class="small-label">Solusi Teratas</p>
|
||||
<h2>Prioritaskan Jepang + Beasiswa</h2>
|
||||
<p>Decision Score 83.8 · Sukses Rate 84%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-pad" id="analisis">
|
||||
<div class="container">
|
||||
<div class="section-kicker">MVP Workspace</div>
|
||||
<h2 class="section-title mb-4">Input kiri, output analisis kanan — seperti prototype Streamlit.</h2>
|
||||
|
||||
{% if messages %}
|
||||
<div class="mb-4">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags|default:'info' }}" role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-4 align-items-start mvp-workspace">
|
||||
<div class="col-lg-4">
|
||||
<div class="form-shell input-panel sticky-lg-top">
|
||||
<div class="panel-heading">
|
||||
<span class="panel-icon">📥</span>
|
||||
<div>
|
||||
<h2>Input Masalah</h2>
|
||||
<p>Masukkan masalah, target, atau hambatan Anda. Contoh: kuliah Singapura/Jepang dengan budget terbatas.</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.description.id_for_label }}">{{ form.description.label }}</label>
|
||||
{{ form.description }}
|
||||
{% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center gap-3">
|
||||
<label class="form-label mb-0" for="{{ form.urgency.id_for_label }}">{{ form.urgency.label }}</label>
|
||||
<output class="urgency-value" id="urgency-output">{{ form.urgency.value|default:3 }}</output>
|
||||
</div>
|
||||
{{ form.urgency }}
|
||||
<div class="range-labels"><span>1</span><span>3</span><span>5</span></div>
|
||||
{% for error in form.urgency.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-optema btn-lg w-100">Jalankan Analisis AI</button>
|
||||
<p class="form-note mb-0 mt-3">Hasil akan disimpan otomatis ke Riwayat Case.</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="analysis-card output-panel">
|
||||
<div class="output-success mb-4">✅ Analisis Berhasil Disimulasikan!</div>
|
||||
|
||||
<section class="output-section">
|
||||
<h2>1. Problem Detection</h2>
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card"><span>Prioritas Skor</span><strong>95/100</strong></div>
|
||||
<div class="metric-card"><span>Dampak / Constraint</span><strong>Budget ketat</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="output-section">
|
||||
<h2>2. Root Cause (Akar Masalah)</h2>
|
||||
<div class="cause-list compact-cause-list">
|
||||
<article>
|
||||
<div class="d-flex justify-content-between gap-3"><strong>Keterbatasan Dana</strong><span>96%</span></div>
|
||||
<div class="progress"><div class="progress-bar score-width-95"></div></div>
|
||||
</article>
|
||||
<article>
|
||||
<div class="d-flex justify-content-between gap-3"><strong>Target Lulus 3 Tahun</strong><span>92%</span></div>
|
||||
<div class="progress"><div class="progress-bar score-width-88"></div></div>
|
||||
</article>
|
||||
<article>
|
||||
<div class="d-flex justify-content-between gap-3"><strong>Biaya Hidup Negara</strong><span>88%</span></div>
|
||||
<div class="progress"><div class="progress-bar score-width-85"></div></div>
|
||||
</article>
|
||||
<article>
|
||||
<div class="d-flex justify-content-between gap-3"><strong>Bahasa & Admission</strong><span>76%</span></div>
|
||||
<div class="progress"><div class="progress-bar score-width-70"></div></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="output-section mb-0">
|
||||
<h2>3. Rekomendasi Solusi & Prediksi Sukses</h2>
|
||||
<p class="formula-note">Decision Score = Impact×0.4 + Efficiency×0.3 + Speed×0.2 + LowRisk×0.1</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle solution-table preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Solusi</th>
|
||||
<th>Impact</th>
|
||||
<th>Efficiency</th>
|
||||
<th>Speed</th>
|
||||
<th>Low Risk</th>
|
||||
<th>Sukses Rate</th>
|
||||
<th>Decision Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><strong>Prioritaskan Jepang + Beasiswa/Part-time Legal</strong></td><td>88</td><td>86</td><td>72</td><td>78</td><td>84%</td><td><strong>83.8</strong></td></tr>
|
||||
<tr><td><strong>Pathway Hemat: Lokal lalu Transfer</strong></td><td>76</td><td>88</td><td>68</td><td>86</td><td>80%</td><td><strong>78.4</strong></td></tr>
|
||||
<tr><td><strong>Singapura Jika Scholarship Besar</strong></td><td>82</td><td>58</td><td>86</td><td>55</td><td>66%</td><td><strong>72.9</strong></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-pad pt-0">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-end mb-4 gap-3 flex-wrap">
|
||||
<div>
|
||||
<div class="section-kicker">Histori Keputusan</div>
|
||||
<h2 class="section-title mb-0">Kasus terbaru</h2>
|
||||
</div>
|
||||
<a href="{% url 'case_list' %}" class="link-optema">Lihat semua kasus →</a>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
{% for case in recent_cases %}
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<a class="case-card h-100" href="{{ case.get_absolute_url }}">
|
||||
<span class="case-area">{{ case.get_business_area_display }}</span>
|
||||
<h3>{{ case.title }}</h3>
|
||||
<p>{{ case.description|truncatechars:88 }}</p>
|
||||
<div class="case-meta"><span>Prioritas {{ case.priority_score }}/100</span><span>{{ case.financial_impact }}</span></div>
|
||||
</a>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="empty-state">
|
||||
<h3>Belum ada kasus tersimpan.</h3>
|
||||
<p>Gunakan form di atas untuk membuat analisis OPTEMA AI pertama Anda.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container d-flex flex-wrap justify-content-between gap-2">
|
||||
<span>OPTEMA AI — Optimal, Effective, Efficient Management Assistant</span>
|
||||
<span>Decision Score = I×0.40 + E×0.30 + S×0.20 + LR×0.10</span>
|
||||
</div>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home
|
||||
from .views import case_detail, case_list, home
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path("cases/", case_list, name="case_list"),
|
||||
path("cases/<int:pk>/", case_detail, name="case_detail"),
|
||||
]
|
||||
|
||||
456
core/views.py
456
core/views.py
@ -1,25 +1,447 @@
|
||||
import os
|
||||
import platform
|
||||
from decimal import Decimal
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.db.models import Prefetch
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from .forms import ProblemCaseForm
|
||||
from .models import ActionPlanStep, ProblemCase, RootCause, SolutionOption
|
||||
|
||||
|
||||
def _clamp(value, minimum=1, maximum=100):
|
||||
return max(minimum, min(maximum, int(value)))
|
||||
|
||||
|
||||
def _has_any(text, keywords):
|
||||
return any(keyword in text for keyword in keywords)
|
||||
|
||||
|
||||
def _decision_score(impact, efficiency, speed, low_risk):
|
||||
return Decimal(str(round((impact * 0.40) + (efficiency * 0.30) + (speed * 0.20) + (low_risk * 0.10), 2)))
|
||||
|
||||
|
||||
def _case_title_from_description(description):
|
||||
first_line = " ".join(description.strip().split())
|
||||
if len(first_line) <= 78:
|
||||
return first_line
|
||||
return f"{first_line[:75].rstrip()}..."
|
||||
|
||||
|
||||
ANALYSIS_DATABASE = [
|
||||
{
|
||||
"kategori": "Pendidikan",
|
||||
"keyword": [
|
||||
"kuliah", "studi", "study", "kampus", "universitas", "university",
|
||||
"beasiswa", "scholarship", "s1", "s2", "sarjana", "bachelor",
|
||||
"master", "singapura", "singapore", "jepang", "japan", "jlpt", "ielts",
|
||||
],
|
||||
"penyebab": [
|
||||
"Budget belum cukup untuk total biaya studi",
|
||||
"Pilihan negara/kampus belum dibandingkan dari total cost of study",
|
||||
"Target lulus 3 tahun butuh validasi kurikulum, bahasa, dan admission",
|
||||
],
|
||||
"solusi": [
|
||||
"Prioritaskan Jepang dengan beasiswa dan kota hemat",
|
||||
"Pilih Singapura hanya jika ada scholarship/subsidy besar",
|
||||
"Buat shortlist program 3 tahun dan simulasi biaya lengkap",
|
||||
],
|
||||
"impact_label": "Budget ketat",
|
||||
"priority": 50,
|
||||
},
|
||||
{
|
||||
"kategori": "Bisnis",
|
||||
"keyword": ["jualan", "penjualan", "produk", "usaha", "bisnis", "toko"],
|
||||
"penyebab": [
|
||||
"Target pasar tidak tepat",
|
||||
"Promosi kurang efektif",
|
||||
"Produk belum sesuai kebutuhan pasar",
|
||||
],
|
||||
"solusi": [
|
||||
"Riset pelanggan",
|
||||
"Perbaiki produk",
|
||||
"Optimasi pemasaran",
|
||||
"Bangun channel penjualan",
|
||||
],
|
||||
"impact_label": "Besar",
|
||||
"priority": 10,
|
||||
},
|
||||
{
|
||||
"kategori": "Keuangan",
|
||||
"keyword": ["uang", "hutang", "utang", "gaji", "tabungan", "biaya"],
|
||||
"penyebab": [
|
||||
"Tidak ada kontrol keuangan",
|
||||
"Pengeluaran terlalu besar",
|
||||
"Pemasukan kurang optimal",
|
||||
],
|
||||
"solusi": [
|
||||
"Buat anggaran",
|
||||
"Kurangi biaya tidak penting",
|
||||
"Cari sumber pemasukan baru",
|
||||
],
|
||||
"impact_label": "Cashflow/budget",
|
||||
"priority": 10,
|
||||
},
|
||||
{
|
||||
"kategori": "Karier",
|
||||
"keyword": ["kerja", "pekerjaan", "cv", "skill", "karir", "karier"],
|
||||
"penyebab": [
|
||||
"Skill tidak sesuai kebutuhan",
|
||||
"Kurang pengalaman",
|
||||
"Portofolio belum kuat",
|
||||
],
|
||||
"solusi": [
|
||||
"Upgrade skill",
|
||||
"Buat portofolio",
|
||||
"Cari peluang kerja sesuai kemampuan",
|
||||
],
|
||||
"impact_label": "Dampak karier",
|
||||
"priority": 10,
|
||||
},
|
||||
{
|
||||
"kategori": "Logistik",
|
||||
"keyword": ["kirim", "pengiriman", "barang", "gudang", "rute"],
|
||||
"penyebab": [
|
||||
"Rute tidak optimal",
|
||||
"Monitoring kurang",
|
||||
"Proses distribusi lambat",
|
||||
],
|
||||
"solusi": [
|
||||
"Optimasi rute",
|
||||
"Tracking barang",
|
||||
"Perbaiki sistem distribusi",
|
||||
],
|
||||
"impact_label": "Operasional",
|
||||
"priority": 10,
|
||||
},
|
||||
{
|
||||
"kategori": "Teknologi",
|
||||
"keyword": ["aplikasi", "website", "coding", "program", "error", "bug"],
|
||||
"penyebab": [
|
||||
"Sistem belum optimal",
|
||||
"Arsitektur kurang tepat",
|
||||
"Bug perangkat lunak",
|
||||
],
|
||||
"solusi": [
|
||||
"Audit sistem",
|
||||
"Perbaiki kode",
|
||||
"Optimasi teknologi",
|
||||
],
|
||||
"impact_label": "Teknis/operasional",
|
||||
"priority": 10,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _detect_problem_category(description):
|
||||
text = description.lower()
|
||||
best_item = None
|
||||
best_score = -1
|
||||
|
||||
for item in ANALYSIS_DATABASE:
|
||||
matches = sum(1 for keyword in item["keyword"] if keyword in text)
|
||||
if matches == 0:
|
||||
continue
|
||||
|
||||
score = matches * 10 + item.get("priority", 0)
|
||||
if item["kategori"] == "Pendidikan" and _has_any(text, ["kuliah", "studi", "kampus", "universitas"]):
|
||||
score += 35
|
||||
if item["kategori"] == "Pendidikan" and _has_any(text, ["singapura", "singapore", "jepang", "japan"]):
|
||||
score += 20
|
||||
|
||||
if score > best_score:
|
||||
best_item = item
|
||||
best_score = score
|
||||
|
||||
return best_item or next(item for item in ANALYSIS_DATABASE if item["kategori"] == "Bisnis")
|
||||
|
||||
|
||||
def _is_education_decision(description):
|
||||
text = description.lower()
|
||||
education_keywords = [
|
||||
"kuliah", "studi", "study", "kampus", "universitas", "university",
|
||||
"beasiswa", "scholarship", "s1", "s2", "sarjana", "bachelor",
|
||||
"master", "singapura", "singapore", "jepang", "japan", "jlpt", "ielts",
|
||||
]
|
||||
constraint_keywords = [
|
||||
"dana", "budget", "biaya", "uang", "200 juta", "200jt", "3 tahun",
|
||||
"tiga tahun", "kelar", "lulus", "visa",
|
||||
]
|
||||
return _has_any(text, education_keywords) and _has_any(text, constraint_keywords)
|
||||
|
||||
|
||||
def _infer_business_area(description):
|
||||
text = description.lower()
|
||||
kategori = _detect_problem_category(description)["kategori"]
|
||||
|
||||
if kategori == "Pendidikan":
|
||||
return ProblemCase.AREA_OTHER
|
||||
if kategori == "Keuangan":
|
||||
return ProblemCase.AREA_FINANCE
|
||||
if kategori == "Karier":
|
||||
return ProblemCase.AREA_PEOPLE
|
||||
if kategori in {"Logistik", "Teknologi"}:
|
||||
return ProblemCase.AREA_OPERATIONS
|
||||
if _has_any(text, ["iklan", "marketing", "konten", "campaign", "kampanye", "promosi"]):
|
||||
return ProblemCase.AREA_MARKETING
|
||||
if _has_any(text, ["produk", "layanan", "fitur", "kualitas"]):
|
||||
return ProblemCase.AREA_PRODUCT
|
||||
return ProblemCase.AREA_SALES
|
||||
|
||||
|
||||
def _write_analysis_records(problem, financial_impact, cause_profiles, options, steps):
|
||||
problem.root_causes.all().delete()
|
||||
problem.solutions.all().delete()
|
||||
|
||||
problem.priority_score = _clamp(80 + problem.urgency * 3)
|
||||
problem.financial_impact = financial_impact
|
||||
problem.status = ProblemCase.STATUS_ANALYZED
|
||||
problem.save(update_fields=["priority_score", "financial_impact", "status", "updated_at"])
|
||||
|
||||
RootCause.objects.bulk_create([
|
||||
RootCause(
|
||||
problem=problem,
|
||||
factor=factor,
|
||||
contribution_score=_clamp(score),
|
||||
why_chain=why_chain,
|
||||
)
|
||||
for factor, score, why_chain in cause_profiles
|
||||
])
|
||||
|
||||
scored = []
|
||||
for option in options:
|
||||
scored.append({
|
||||
**option,
|
||||
"decision_score": _decision_score(
|
||||
option["impact"],
|
||||
option["efficiency"],
|
||||
option["speed"],
|
||||
option["low_risk"],
|
||||
),
|
||||
})
|
||||
scored.sort(key=lambda item: item["decision_score"], reverse=True)
|
||||
|
||||
created_solutions = SolutionOption.objects.bulk_create([
|
||||
SolutionOption(
|
||||
problem=problem,
|
||||
title=option["title"],
|
||||
impact=option["impact"],
|
||||
efficiency=option["efficiency"],
|
||||
speed=option["speed"],
|
||||
low_risk=option["low_risk"],
|
||||
decision_score=option["decision_score"],
|
||||
success_rate=option["success_rate"],
|
||||
rank=rank,
|
||||
rationale=option["rationale"],
|
||||
)
|
||||
for rank, option in enumerate(scored, start=1)
|
||||
])
|
||||
|
||||
top_solution = created_solutions[0]
|
||||
ActionPlanStep.objects.bulk_create([
|
||||
ActionPlanStep(solution=top_solution, day_index=day, title=title, task=task)
|
||||
for day, title, task in steps
|
||||
])
|
||||
|
||||
|
||||
def _build_education_analysis(problem):
|
||||
cause_profiles = [
|
||||
(
|
||||
"Keterbatasan Dana",
|
||||
96,
|
||||
"Budget perlu menutup tuition, biaya hidup, visa, tiket, asuransi, dan dana darurat; jadi pilihan negara/kampus harus disaring dari batas biaya dulu.",
|
||||
),
|
||||
(
|
||||
"Target Lulus 3 Tahun",
|
||||
92,
|
||||
"Target selesai cepat hanya realistis jika program, credit transfer, kalender akademik, dan syarat kelulusan cocok sejak awal.",
|
||||
),
|
||||
(
|
||||
"Biaya Hidup Negara",
|
||||
88,
|
||||
"Perbandingan Singapura vs Jepang harus dihitung dari total cost of study, bukan hanya uang kuliah; kota, tempat tinggal, dan transport sangat menentukan.",
|
||||
),
|
||||
(
|
||||
"Bahasa & Admission",
|
||||
76,
|
||||
"Risiko gagal masuk atau molor muncul jika syarat IELTS/JLPT, dokumen, deadline, dan kesiapan bahasa belum dipetakan.",
|
||||
),
|
||||
]
|
||||
options = [
|
||||
{
|
||||
"title": "Prioritaskan Jepang + Beasiswa/Part-time Legal",
|
||||
"impact": 88,
|
||||
"efficiency": 86,
|
||||
"speed": 72,
|
||||
"low_risk": 78,
|
||||
"success_rate": 84,
|
||||
"rationale": "Lebih masuk akal untuk budget ketat bila shortlist difokuskan ke kampus/program yang punya beasiswa, kota yang lebih hemat, dan opsi kerja paruh waktu sesuai aturan visa.",
|
||||
},
|
||||
{
|
||||
"title": "Singapura Hanya Jika Ada Scholarship/Subsidy Besar",
|
||||
"impact": 82,
|
||||
"efficiency": 58,
|
||||
"speed": 86,
|
||||
"low_risk": 55,
|
||||
"success_rate": 66,
|
||||
"rationale": "Singapura bisa unggul untuk akses industri dan durasi cepat, tetapi budget 200 juta berisiko tidak cukup tanpa bantuan biaya yang jelas sejak awal.",
|
||||
},
|
||||
{
|
||||
"title": "Pathway Hemat: Mulai Lokal lalu Transfer/Credit Recognition",
|
||||
"impact": 76,
|
||||
"efficiency": 88,
|
||||
"speed": 68,
|
||||
"low_risk": 86,
|
||||
"success_rate": 80,
|
||||
"rationale": "Menekan cash-out awal sambil mengejar beasiswa dan kesiapan bahasa, tetapi harus dipastikan kredit bisa diakui agar target total 3 tahun tidak mundur.",
|
||||
},
|
||||
]
|
||||
steps = [
|
||||
(1, "Buat batas biaya final", "Pecah dana 200 juta menjadi tuition, living cost, visa, tiket, asuransi, dan dana darurat; coret opsi yang melewati batas aman."),
|
||||
(2, "Shortlist program 3 tahun", "Cari minimal 10 program Jepang dan 3 program Singapura yang durasinya cocok, lalu tandai syarat bahasa, deadline, dan total biaya."),
|
||||
(3, "Kejar funding", "Daftar beasiswa, tuition waiver, atau sponsor; jangan memilih Singapura kecuali kekurangan biaya sudah tertutup jelas."),
|
||||
(4, "Validasi aturan visa", "Cek izin kerja paruh waktu, syarat dokumen finansial, serta risiko jika kurs/biaya hidup naik."),
|
||||
(5, "Decision gate", "Pilih Jepang jika total biaya paling aman; pilih Singapura hanya jika scholarship/subsidy membuat biaya 3 tahun masuk budget."),
|
||||
]
|
||||
_write_analysis_records(problem, "Budget ketat", cause_profiles, options, steps)
|
||||
|
||||
|
||||
def _build_category_analysis(problem, category):
|
||||
kategori = category["kategori"]
|
||||
causes = category["penyebab"]
|
||||
solutions = category["solusi"]
|
||||
|
||||
cause_profiles = [
|
||||
(
|
||||
cause,
|
||||
_clamp(92 - index * 8),
|
||||
f"Input terdeteksi sebagai kategori {kategori}. Penyebab ini perlu divalidasi karena dapat langsung memengaruhi target, batasan, dan keputusan berikutnya.",
|
||||
)
|
||||
for index, cause in enumerate(causes)
|
||||
]
|
||||
|
||||
options = []
|
||||
for index, solution in enumerate(solutions):
|
||||
options.append({
|
||||
"title": solution,
|
||||
"impact": _clamp(88 - index * 4),
|
||||
"efficiency": _clamp(82 + (index % 2) * 5 - index * 2),
|
||||
"speed": _clamp(84 - index * 5),
|
||||
"low_risk": _clamp(74 + index * 4),
|
||||
"success_rate": _clamp(82 - index * 3),
|
||||
"rationale": f"Solusi ini cocok untuk kategori {kategori} karena langsung menargetkan penyebab utama: {causes[min(index, len(causes) - 1)]}.",
|
||||
})
|
||||
|
||||
steps = [
|
||||
(1, "Kunci tujuan dan batasan", f"Kategori utama: {kategori}. Tulis target akhir, batasan dana/waktu, dan indikator sukses yang terukur."),
|
||||
(2, "Validasi penyebab utama", f"Cek apakah penyebab paling dominan adalah: {causes[0]}. Kumpulkan bukti sederhana sebelum memilih solusi."),
|
||||
(3, "Bandingkan opsi solusi", f"Bandingkan opsi: {', '.join(solutions[:3])}. Pilih yang paling sesuai dengan budget, waktu, dan risiko."),
|
||||
(4, "Jalankan langkah kecil", "Mulai dari eksperimen paling kecil selama 1-3 hari agar cepat terlihat apakah arah solusi benar."),
|
||||
(5, "Evaluasi dan putuskan", "Bandingkan hasil dengan indikator sukses, lalu pilih lanjutkan, ubah strategi, atau hentikan opsi yang tidak efektif."),
|
||||
]
|
||||
_write_analysis_records(problem, category.get("impact_label", "Sedang"), cause_profiles, options, steps)
|
||||
|
||||
|
||||
def _build_business_analysis(problem):
|
||||
cause_profiles = [
|
||||
("Marketing", 95, "Akuisisi dan pesan penjualan belum cukup tajam untuk menjangkau segmen yang paling siap membeli."),
|
||||
("Kompetitor", 88, "Penawaran kompetitor terlihat lebih kuat sehingga pelanggan membandingkan harga, bonus, dan bukti hasil."),
|
||||
("Harga", 85, "Persepsi value belum seimbang dengan harga; bundling, bonus, atau bukti manfaat perlu diperjelas."),
|
||||
("Produk", 70, "Produk/layanan perlu validasi ulang dari feedback pelanggan agar lebih relevan dengan kebutuhan saat ini."),
|
||||
]
|
||||
options = [
|
||||
{
|
||||
"title": "Optimasi Iklan Digital",
|
||||
"impact": 90,
|
||||
"efficiency": 70,
|
||||
"speed": 85,
|
||||
"low_risk": 60,
|
||||
"success_rate": 85,
|
||||
"rationale": "Perbaiki targeting, materi iklan, dan pesan promosi agar trafik yang masuk lebih relevan dan cepat terukur.",
|
||||
},
|
||||
{
|
||||
"title": "Program Reseller & Agen",
|
||||
"impact": 85,
|
||||
"efficiency": 80,
|
||||
"speed": 60,
|
||||
"low_risk": 75,
|
||||
"success_rate": 92,
|
||||
"rationale": "Bangun kanal distribusi rendah biaya melalui mitra yang sudah punya relasi dan kepercayaan dengan calon pembeli.",
|
||||
},
|
||||
{
|
||||
"title": "Diskon Bundling Akhir Bulan",
|
||||
"impact": 70,
|
||||
"efficiency": 60,
|
||||
"speed": 95,
|
||||
"low_risk": 90,
|
||||
"success_rate": 70,
|
||||
"rationale": "Dorong keputusan pembelian cepat dengan paket bernilai tinggi, periode terbatas, dan risiko operasional rendah.",
|
||||
},
|
||||
]
|
||||
steps = [
|
||||
(1, "Audit data cepat", "Kumpulkan angka penjualan, biaya iklan, channel, produk terlaris, dan keluhan pelanggan 90 hari terakhir."),
|
||||
(2, "Validasi akar masalah", "Bandingkan temuan dengan faktor Marketing, Kompetitor, Harga, dan Produk; pilih 1–2 penyebab paling dominan."),
|
||||
(3, "Luncurkan eksperimen", "Jalankan eksperimen kecil untuk solusi teratas dengan target metrik, owner, dan batas anggaran yang jelas."),
|
||||
(4, "Pantau respon pasar", "Review metrik harian: leads, conversion rate, biaya per transaksi, repeat order, dan risiko operasional."),
|
||||
(5, "Putuskan scale/stop", "Bandingkan hasil dengan baseline lalu pilih: scale, iterasi pesan/offer, atau hentikan eksperimen."),
|
||||
]
|
||||
_write_analysis_records(problem, "Besar", cause_profiles, options, steps)
|
||||
|
||||
|
||||
def build_case_analysis(problem):
|
||||
"""Create deterministic MVP analysis records with category-aware rules."""
|
||||
category = _detect_problem_category(problem.description)
|
||||
|
||||
if category["kategori"] == "Pendidikan" and _is_education_decision(problem.description):
|
||||
_build_education_analysis(problem)
|
||||
return
|
||||
|
||||
_build_category_analysis(problem, category)
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
form = ProblemCaseForm(request.POST or None)
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
with transaction.atomic():
|
||||
problem = form.save(commit=False)
|
||||
problem.title = _case_title_from_description(problem.description)
|
||||
problem.business_area = _infer_business_area(problem.description)
|
||||
problem.status = ProblemCase.STATUS_DRAFT
|
||||
problem.save()
|
||||
build_case_analysis(problem)
|
||||
messages.success(request, "✅ Analisis berhasil disimulasikan dan disimpan sebagai case.")
|
||||
return redirect(problem.get_absolute_url())
|
||||
messages.error(request, "Mohon periksa input. Deskripsi masalah perlu cukup lengkap agar analisis akurat.")
|
||||
|
||||
recent_cases = ProblemCase.objects.all()[:4]
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
"form": form,
|
||||
"recent_cases": recent_cases,
|
||||
"page_title": "OPTEMA AI — MVP Decision Intelligence",
|
||||
"meta_description": "OPTEMA AI membantu mengubah masalah bisnis, keuangan, karier, logistik, teknologi, dan pendidikan menjadi problem detection, root-cause analysis, decision scoring, dan action plan.",
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
|
||||
|
||||
def case_list(request):
|
||||
cases = ProblemCase.objects.all()
|
||||
return render(request, "core/case_list.html", {
|
||||
"cases": cases,
|
||||
"page_title": "Daftar Kasus — OPTEMA AI",
|
||||
"meta_description": "Lihat histori kasus yang sudah dianalisis oleh OPTEMA AI.",
|
||||
})
|
||||
|
||||
|
||||
def case_detail(request, pk):
|
||||
action_steps = Prefetch("solutions__action_steps", queryset=ActionPlanStep.objects.all())
|
||||
problem = get_object_or_404(
|
||||
ProblemCase.objects.prefetch_related("root_causes", "solutions", action_steps),
|
||||
pk=pk,
|
||||
)
|
||||
top_solution = problem.solutions.first()
|
||||
return render(request, "core/case_detail.html", {
|
||||
"problem": problem,
|
||||
"top_solution": top_solution,
|
||||
"page_title": f"{problem.title} — Analisis OPTEMA AI",
|
||||
"meta_description": f"Analisis prioritas, akar masalah, solusi berskor, dan action plan untuk {problem.title}.",
|
||||
})
|
||||
|
||||
@ -1,4 +1,429 @@
|
||||
/* Custom styles for the application */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
/* OPTEMA AI custom brand system */
|
||||
:root {
|
||||
--optema-ink: #14213d;
|
||||
--optema-muted: #627084;
|
||||
--optema-primary: #0f8b8d;
|
||||
--optema-primary-dark: #0b6768;
|
||||
--optema-secondary: #f6ae2d;
|
||||
--optema-accent: #f26419;
|
||||
--optema-mint: #dff8f5;
|
||||
--optema-sand: #fff6e4;
|
||||
--optema-cloud: #f7fafc;
|
||||
--optema-white: #ffffff;
|
||||
--optema-line: #dce8ee;
|
||||
--optema-shadow: 0 24px 70px rgba(20, 33, 61, 0.14);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
color: var(--optema-ink);
|
||||
background:
|
||||
radial-gradient(circle at 8% 4%, rgba(15, 139, 141, 0.14), transparent 28rem),
|
||||
radial-gradient(circle at 95% 8%, rgba(246, 174, 45, 0.16), transparent 24rem),
|
||||
var(--optema-cloud);
|
||||
}
|
||||
|
||||
h1, h2, h3, .navbar-brand {
|
||||
font-family: 'Plus Jakarta Sans', 'Inter', sans-serif;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
a { color: inherit; }
|
||||
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible {
|
||||
outline: 3px solid rgba(246, 174, 45, 0.65) !important;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.optema-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: rgba(247, 250, 252, 0.82);
|
||||
backdrop-filter: blur(18px);
|
||||
border-bottom: 1px solid rgba(220, 232, 238, 0.82);
|
||||
}
|
||||
.brand-mark { font-weight: 800; color: var(--optema-ink); }
|
||||
.brand-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 2.15rem; height: 2.15rem;
|
||||
margin-right: .45rem;
|
||||
border-radius: 14px;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--optema-primary), var(--optema-accent));
|
||||
box-shadow: 0 12px 24px rgba(15, 139, 141, 0.24);
|
||||
}
|
||||
.nav-link { font-weight: 700; color: var(--optema-muted); }
|
||||
.nav-link:hover, .nav-link:focus { color: var(--optema-primary); }
|
||||
.admin-link {
|
||||
border: 1px solid var(--optema-line);
|
||||
border-radius: 999px;
|
||||
padding-inline: 1rem !important;
|
||||
background: rgba(255,255,255,.7);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
min-height: 78vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(223,248,245,.88), rgba(255,246,228,.72) 55%, rgba(255,255,255,.9)),
|
||||
var(--optema-cloud);
|
||||
}
|
||||
.shape {
|
||||
position: absolute; border-radius: 999px; filter: blur(.2px); opacity: .82;
|
||||
}
|
||||
.shape-one {
|
||||
width: 15rem; height: 15rem; right: 6%; top: 12%;
|
||||
background: linear-gradient(135deg, rgba(15,139,141,.2), rgba(246,174,45,.38));
|
||||
}
|
||||
.shape-two {
|
||||
width: 9rem; height: 9rem; left: 4%; bottom: 8%;
|
||||
background: linear-gradient(135deg, rgba(242,100,25,.22), rgba(15,139,141,.2));
|
||||
}
|
||||
.eyebrow, .section-kicker {
|
||||
color: var(--optema-primary-dark);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .12em;
|
||||
font-size: .78rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.display-heading {
|
||||
font-size: clamp(2.7rem, 6vw, 5.9rem);
|
||||
line-height: .95;
|
||||
font-weight: 800;
|
||||
}
|
||||
.hero-copy, .section-copy {
|
||||
color: var(--optema-muted);
|
||||
font-size: clamp(1rem, 1vw + .76rem, 1.2rem);
|
||||
line-height: 1.75;
|
||||
}
|
||||
.btn-optema {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
border-radius: 999px;
|
||||
padding: .9rem 1.35rem;
|
||||
background: linear-gradient(135deg, var(--optema-primary), var(--optema-primary-dark));
|
||||
box-shadow: 0 14px 35px rgba(15, 139, 141, .26);
|
||||
}
|
||||
.btn-optema:hover, .btn-optema:focus { color:#fff; transform: translateY(-1px); box-shadow: 0 18px 44px rgba(15,139,141,.32); }
|
||||
.btn-outline-optema {
|
||||
border: 1px solid rgba(15,139,141,.3);
|
||||
color: var(--optema-primary-dark);
|
||||
background: rgba(255,255,255,.68);
|
||||
font-weight: 800;
|
||||
border-radius: 999px;
|
||||
padding: .9rem 1.35rem;
|
||||
}
|
||||
.btn-outline-optema:hover, .btn-outline-optema:focus { background: var(--optema-mint); color: var(--optema-primary-dark); }
|
||||
.trust-strip { display: flex; flex-wrap: wrap; gap: .65rem; }
|
||||
.trust-strip span, .score-pill, .status-dot {
|
||||
display: inline-flex; align-items: center; gap: .45rem;
|
||||
border: 1px solid rgba(15,139,141,.16);
|
||||
background: rgba(255,255,255,.72);
|
||||
border-radius: 999px;
|
||||
padding: .55rem .8rem;
|
||||
color: var(--optema-muted);
|
||||
font-weight: 800;
|
||||
font-size: .86rem;
|
||||
}
|
||||
.status-dot i { width: .55rem; height: .55rem; border-radius: 999px; background: #23c483; box-shadow: 0 0 0 6px rgba(35,196,131,.15); }
|
||||
.glass-card, .form-shell, .analysis-card, .empty-state, .case-card, .metric-card {
|
||||
border: 1px solid rgba(220, 232, 238, .9);
|
||||
background: rgba(255,255,255,.82);
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: var(--optema-shadow);
|
||||
border-radius: 32px;
|
||||
}
|
||||
.decision-panel { padding: clamp(1.35rem, 3vw, 2rem); }
|
||||
.mini-chart { display: grid; gap: 1rem; }
|
||||
.mini-chart div {
|
||||
--bar: 70%;
|
||||
position: relative; overflow: hidden;
|
||||
display: flex; justify-content: space-between;
|
||||
padding: .9rem 1rem;
|
||||
border-radius: 18px;
|
||||
background: #eef6f7;
|
||||
font-weight: 800;
|
||||
}
|
||||
.mini-chart div::before {
|
||||
content: ''; position: absolute; inset: 0 auto 0 0; width: var(--bar);
|
||||
background: linear-gradient(90deg, rgba(15,139,141,.24), rgba(246,174,45,.28));
|
||||
}
|
||||
.mini-chart span, .mini-chart b { position: relative; z-index: 1; }
|
||||
.bar-95 { --bar: 95% !important; } .bar-88 { --bar: 88% !important; } .bar-82 { --bar: 82% !important; }
|
||||
.solution-preview {
|
||||
padding: 1.2rem; border-radius: 24px;
|
||||
background: linear-gradient(135deg, var(--optema-ink), #183b56); color: white;
|
||||
}
|
||||
.solution-preview h2 { font-size: 1.35rem; }
|
||||
.solution-preview p { color: rgba(255,255,255,.74); margin-bottom: 0; }
|
||||
.small-label { color: var(--optema-secondary) !important; text-transform: uppercase; font-weight: 800; letter-spacing: .12em; font-size: .75rem; }
|
||||
|
||||
.section-pad { padding: clamp(4rem, 8vw, 7rem) 0; }
|
||||
.section-title, .page-title { font-size: clamp(2rem, 3.5vw, 3.6rem); font-weight: 800; line-height: 1.02; }
|
||||
.form-shell { padding: clamp(1.25rem, 3vw, 2rem); }
|
||||
.form-control, .form-select { border: 1px solid var(--optema-line); border-radius: 18px; padding: .9rem 1rem; }
|
||||
.form-control:focus, .form-select:focus { border-color: var(--optema-primary); box-shadow: 0 0 0 .22rem rgba(15,139,141,.13); }
|
||||
.form-label { font-weight: 800; color: var(--optema-ink); }
|
||||
.form-text { color: var(--optema-muted); }
|
||||
.feature-list { display: grid; gap: .85rem; margin-top: 1.5rem; }
|
||||
.feature-list div { display: flex; gap: .8rem; align-items: center; color: var(--optema-muted); }
|
||||
.feature-list strong {
|
||||
display:grid; place-items:center; width:2rem; height:2rem; border-radius:12px;
|
||||
background: var(--optema-sand); color: var(--optema-accent);
|
||||
}
|
||||
.case-card {
|
||||
display: flex; flex-direction: column; gap: .9rem;
|
||||
padding: 1.35rem; text-decoration: none; transition: .2s ease;
|
||||
}
|
||||
.case-card:hover, .case-card:focus { transform: translateY(-4px); border-color: rgba(15,139,141,.35); color: var(--optema-ink); }
|
||||
.case-card h2, .case-card h3 { font-size: 1.15rem; font-weight: 800; margin: 0; }
|
||||
.case-card p { color: var(--optema-muted); margin: 0; line-height: 1.65; }
|
||||
.case-area { color: var(--optema-primary-dark); font-size: .78rem; font-weight: 900; text-transform: uppercase; letter-spacing: .1em; }
|
||||
.case-meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .7rem; color: var(--optema-muted); font-weight: 800; font-size: .86rem; margin-top: auto; }
|
||||
.empty-state { padding: 2rem; text-align: center; }
|
||||
.empty-state p { color: var(--optema-muted); }
|
||||
.link-optema { color: var(--optema-primary-dark); font-weight: 900; text-decoration: none; }
|
||||
.link-optema:hover { color: var(--optema-accent); }
|
||||
.site-footer {
|
||||
padding: 1.5rem 0; color: var(--optema-muted);
|
||||
border-top: 1px solid var(--optema-line); background: rgba(255,255,255,.68);
|
||||
}
|
||||
.page-shell { min-height: 100vh; background: linear-gradient(180deg, rgba(223,248,245,.55), transparent 24rem); }
|
||||
.detail-hero { padding: clamp(3rem, 6vw, 5rem) 0; background: linear-gradient(135deg, rgba(223,248,245,.9), rgba(255,246,228,.85)); }
|
||||
.metric-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1rem; }
|
||||
.metric-card { padding: 1.25rem; }
|
||||
.metric-card span { color: var(--optema-muted); font-weight: 800; display:block; }
|
||||
.metric-card strong { font-family:'Plus Jakarta Sans'; font-size: 1.75rem; }
|
||||
.analysis-card { padding: clamp(1.2rem, 2.6vw, 2rem); }
|
||||
.analysis-card h2 { font-weight: 800; margin-bottom: 1.2rem; }
|
||||
.cause-list { display: grid; gap: 1.2rem; }
|
||||
.cause-list article p { color: var(--optema-muted); margin: .6rem 0 0; line-height: 1.6; }
|
||||
.progress { height: .65rem; background: #e6f2f3; border-radius: 999px; margin-top: .55rem; }
|
||||
.progress-bar { background: linear-gradient(90deg, var(--optema-primary), var(--optema-secondary)); }
|
||||
.solution-table { --bs-table-bg: transparent; }
|
||||
.solution-table th { color: var(--optema-muted); text-transform: uppercase; letter-spacing: .08em; font-size: .78rem; }
|
||||
.solution-table td { padding: 1rem .75rem; }
|
||||
.solution-table p { color: var(--optema-muted); margin: .25rem 0; }
|
||||
.solution-table small { color: var(--optema-primary-dark); font-weight: 800; }
|
||||
.rank-badge { display: inline-flex; border-radius: 999px; background: var(--optema-sand); color: var(--optema-accent); padding: .35rem .6rem; font-weight: 900; }
|
||||
.action-card { background: linear-gradient(145deg, rgba(255,255,255,.92), rgba(223,248,245,.76)); }
|
||||
.timeline { display: grid; gap: 1rem; }
|
||||
.timeline article { display: grid; grid-template-columns: 5rem 1fr; gap: 1rem; align-items: start; }
|
||||
.timeline article > span {
|
||||
display: inline-flex; justify-content:center; padding: .45rem .65rem; border-radius: 999px;
|
||||
background: var(--optema-ink); color: white; font-weight: 900; font-size: .82rem;
|
||||
}
|
||||
.timeline h3 { font-size: 1.05rem; font-weight: 800; margin-bottom: .2rem; }
|
||||
.timeline p { color: var(--optema-muted); margin: 0; line-height: 1.6; }
|
||||
|
||||
.score-width-0 { width: 0%; }
|
||||
.score-width-1 { width: 1%; }
|
||||
.score-width-2 { width: 2%; }
|
||||
.score-width-3 { width: 3%; }
|
||||
.score-width-4 { width: 4%; }
|
||||
.score-width-5 { width: 5%; }
|
||||
.score-width-6 { width: 6%; }
|
||||
.score-width-7 { width: 7%; }
|
||||
.score-width-8 { width: 8%; }
|
||||
.score-width-9 { width: 9%; }
|
||||
.score-width-10 { width: 10%; }
|
||||
.score-width-11 { width: 11%; }
|
||||
.score-width-12 { width: 12%; }
|
||||
.score-width-13 { width: 13%; }
|
||||
.score-width-14 { width: 14%; }
|
||||
.score-width-15 { width: 15%; }
|
||||
.score-width-16 { width: 16%; }
|
||||
.score-width-17 { width: 17%; }
|
||||
.score-width-18 { width: 18%; }
|
||||
.score-width-19 { width: 19%; }
|
||||
.score-width-20 { width: 20%; }
|
||||
.score-width-21 { width: 21%; }
|
||||
.score-width-22 { width: 22%; }
|
||||
.score-width-23 { width: 23%; }
|
||||
.score-width-24 { width: 24%; }
|
||||
.score-width-25 { width: 25%; }
|
||||
.score-width-26 { width: 26%; }
|
||||
.score-width-27 { width: 27%; }
|
||||
.score-width-28 { width: 28%; }
|
||||
.score-width-29 { width: 29%; }
|
||||
.score-width-30 { width: 30%; }
|
||||
.score-width-31 { width: 31%; }
|
||||
.score-width-32 { width: 32%; }
|
||||
.score-width-33 { width: 33%; }
|
||||
.score-width-34 { width: 34%; }
|
||||
.score-width-35 { width: 35%; }
|
||||
.score-width-36 { width: 36%; }
|
||||
.score-width-37 { width: 37%; }
|
||||
.score-width-38 { width: 38%; }
|
||||
.score-width-39 { width: 39%; }
|
||||
.score-width-40 { width: 40%; }
|
||||
.score-width-41 { width: 41%; }
|
||||
.score-width-42 { width: 42%; }
|
||||
.score-width-43 { width: 43%; }
|
||||
.score-width-44 { width: 44%; }
|
||||
.score-width-45 { width: 45%; }
|
||||
.score-width-46 { width: 46%; }
|
||||
.score-width-47 { width: 47%; }
|
||||
.score-width-48 { width: 48%; }
|
||||
.score-width-49 { width: 49%; }
|
||||
.score-width-50 { width: 50%; }
|
||||
.score-width-51 { width: 51%; }
|
||||
.score-width-52 { width: 52%; }
|
||||
.score-width-53 { width: 53%; }
|
||||
.score-width-54 { width: 54%; }
|
||||
.score-width-55 { width: 55%; }
|
||||
.score-width-56 { width: 56%; }
|
||||
.score-width-57 { width: 57%; }
|
||||
.score-width-58 { width: 58%; }
|
||||
.score-width-59 { width: 59%; }
|
||||
.score-width-60 { width: 60%; }
|
||||
.score-width-61 { width: 61%; }
|
||||
.score-width-62 { width: 62%; }
|
||||
.score-width-63 { width: 63%; }
|
||||
.score-width-64 { width: 64%; }
|
||||
.score-width-65 { width: 65%; }
|
||||
.score-width-66 { width: 66%; }
|
||||
.score-width-67 { width: 67%; }
|
||||
.score-width-68 { width: 68%; }
|
||||
.score-width-69 { width: 69%; }
|
||||
.score-width-70 { width: 70%; }
|
||||
.score-width-71 { width: 71%; }
|
||||
.score-width-72 { width: 72%; }
|
||||
.score-width-73 { width: 73%; }
|
||||
.score-width-74 { width: 74%; }
|
||||
.score-width-75 { width: 75%; }
|
||||
.score-width-76 { width: 76%; }
|
||||
.score-width-77 { width: 77%; }
|
||||
.score-width-78 { width: 78%; }
|
||||
.score-width-79 { width: 79%; }
|
||||
.score-width-80 { width: 80%; }
|
||||
.score-width-81 { width: 81%; }
|
||||
.score-width-82 { width: 82%; }
|
||||
.score-width-83 { width: 83%; }
|
||||
.score-width-84 { width: 84%; }
|
||||
.score-width-85 { width: 85%; }
|
||||
.score-width-86 { width: 86%; }
|
||||
.score-width-87 { width: 87%; }
|
||||
.score-width-88 { width: 88%; }
|
||||
.score-width-89 { width: 89%; }
|
||||
.score-width-90 { width: 90%; }
|
||||
.score-width-91 { width: 91%; }
|
||||
.score-width-92 { width: 92%; }
|
||||
.score-width-93 { width: 93%; }
|
||||
.score-width-94 { width: 94%; }
|
||||
.score-width-95 { width: 95%; }
|
||||
.score-width-96 { width: 96%; }
|
||||
.score-width-97 { width: 97%; }
|
||||
.score-width-98 { width: 98%; }
|
||||
.score-width-99 { width: 99%; }
|
||||
.score-width-100 { width: 100%; }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.metric-grid { grid-template-columns: 1fr; }
|
||||
.timeline article { grid-template-columns: 1fr; }
|
||||
.hero-section { min-height: auto; }
|
||||
}
|
||||
|
||||
|
||||
/* Streamlit-inspired MVP workspace */
|
||||
.hero-subtitle {
|
||||
font-family: 'Plus Jakarta Sans', 'Inter', sans-serif;
|
||||
font-size: clamp(1.25rem, 1.2vw + 1rem, 2rem);
|
||||
font-weight: 800;
|
||||
color: var(--optema-primary-dark);
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.mvp-hero .display-heading { letter-spacing: -0.07em; }
|
||||
.mvp-workspace .sticky-lg-top { top: 6rem; }
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
.panel-heading h2 {
|
||||
font-size: 1.45rem;
|
||||
margin: 0 0 .25rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
.panel-heading p,
|
||||
.form-note,
|
||||
.formula-note,
|
||||
.detail-status-card p {
|
||||
color: var(--optema-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.panel-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, var(--optema-mint), var(--optema-sand));
|
||||
box-shadow: 0 16px 28px rgba(15, 139, 141, .12);
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
.problem-textarea {
|
||||
min-height: 15rem;
|
||||
resize: vertical;
|
||||
border-radius: 22px;
|
||||
border-color: rgba(15,139,141,.18);
|
||||
background: rgba(255,255,255,.82);
|
||||
}
|
||||
.problem-textarea:focus {
|
||||
border-color: var(--optema-primary);
|
||||
box-shadow: 0 0 0 .25rem rgba(15,139,141,.12);
|
||||
}
|
||||
.urgency-slider { accent-color: var(--optema-primary); }
|
||||
.urgency-value {
|
||||
min-width: 2.4rem;
|
||||
text-align: center;
|
||||
padding: .35rem .7rem;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
background: var(--optema-ink);
|
||||
font-weight: 900;
|
||||
}
|
||||
.range-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--optema-muted);
|
||||
font-size: .82rem;
|
||||
font-weight: 800;
|
||||
margin-top: -.3rem;
|
||||
}
|
||||
.output-panel { overflow: hidden; }
|
||||
.output-success {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
color: #087447;
|
||||
background: rgba(35, 196, 131, .12);
|
||||
border: 1px solid rgba(35, 196, 131, .22);
|
||||
border-radius: 999px;
|
||||
padding: .7rem 1rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
.output-section {
|
||||
padding: 1.25rem 0;
|
||||
border-top: 1px solid var(--optema-line);
|
||||
}
|
||||
.output-section:first-of-type { border-top: 0; padding-top: 0; }
|
||||
.output-section h2 { font-size: clamp(1.25rem, 1vw + 1rem, 1.8rem); }
|
||||
.compact-cause-list { gap: .85rem; }
|
||||
.compact-cause-list article p { display: none; }
|
||||
.preview-table th,
|
||||
.preview-table td { white-space: nowrap; }
|
||||
.detail-status-card { padding: 1.35rem; }
|
||||
.bar-85 { --bar: 85% !important; }
|
||||
.bar-70 { --bar: 70% !important; }
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.mvp-workspace .sticky-lg-top { position: static; }
|
||||
}
|
||||
|
||||
@ -1,21 +1,429 @@
|
||||
|
||||
/* OPTEMA AI custom brand system */
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
--optema-ink: #14213d;
|
||||
--optema-muted: #627084;
|
||||
--optema-primary: #0f8b8d;
|
||||
--optema-primary-dark: #0b6768;
|
||||
--optema-secondary: #f6ae2d;
|
||||
--optema-accent: #f26419;
|
||||
--optema-mint: #dff8f5;
|
||||
--optema-sand: #fff6e4;
|
||||
--optema-cloud: #f7fafc;
|
||||
--optema-white: #ffffff;
|
||||
--optema-line: #dce8ee;
|
||||
--optema-shadow: 0 24px 70px rgba(20, 33, 61, 0.14);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
color: var(--optema-ink);
|
||||
background:
|
||||
radial-gradient(circle at 8% 4%, rgba(15, 139, 141, 0.14), transparent 28rem),
|
||||
radial-gradient(circle at 95% 8%, rgba(246, 174, 45, 0.16), transparent 24rem),
|
||||
var(--optema-cloud);
|
||||
}
|
||||
|
||||
h1, h2, h3, .navbar-brand {
|
||||
font-family: 'Plus Jakarta Sans', 'Inter', sans-serif;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
a { color: inherit; }
|
||||
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible {
|
||||
outline: 3px solid rgba(246, 174, 45, 0.65) !important;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.optema-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: rgba(247, 250, 252, 0.82);
|
||||
backdrop-filter: blur(18px);
|
||||
border-bottom: 1px solid rgba(220, 232, 238, 0.82);
|
||||
}
|
||||
.brand-mark { font-weight: 800; color: var(--optema-ink); }
|
||||
.brand-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 2.15rem; height: 2.15rem;
|
||||
margin-right: .45rem;
|
||||
border-radius: 14px;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--optema-primary), var(--optema-accent));
|
||||
box-shadow: 0 12px 24px rgba(15, 139, 141, 0.24);
|
||||
}
|
||||
.nav-link { font-weight: 700; color: var(--optema-muted); }
|
||||
.nav-link:hover, .nav-link:focus { color: var(--optema-primary); }
|
||||
.admin-link {
|
||||
border: 1px solid var(--optema-line);
|
||||
border-radius: 999px;
|
||||
padding-inline: 1rem !important;
|
||||
background: rgba(255,255,255,.7);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
min-height: 78vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(223,248,245,.88), rgba(255,246,228,.72) 55%, rgba(255,255,255,.9)),
|
||||
var(--optema-cloud);
|
||||
}
|
||||
.shape {
|
||||
position: absolute; border-radius: 999px; filter: blur(.2px); opacity: .82;
|
||||
}
|
||||
.shape-one {
|
||||
width: 15rem; height: 15rem; right: 6%; top: 12%;
|
||||
background: linear-gradient(135deg, rgba(15,139,141,.2), rgba(246,174,45,.38));
|
||||
}
|
||||
.shape-two {
|
||||
width: 9rem; height: 9rem; left: 4%; bottom: 8%;
|
||||
background: linear-gradient(135deg, rgba(242,100,25,.22), rgba(15,139,141,.2));
|
||||
}
|
||||
.eyebrow, .section-kicker {
|
||||
color: var(--optema-primary-dark);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .12em;
|
||||
font-size: .78rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.display-heading {
|
||||
font-size: clamp(2.7rem, 6vw, 5.9rem);
|
||||
line-height: .95;
|
||||
font-weight: 800;
|
||||
}
|
||||
.hero-copy, .section-copy {
|
||||
color: var(--optema-muted);
|
||||
font-size: clamp(1rem, 1vw + .76rem, 1.2rem);
|
||||
line-height: 1.75;
|
||||
}
|
||||
.btn-optema {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
border-radius: 999px;
|
||||
padding: .9rem 1.35rem;
|
||||
background: linear-gradient(135deg, var(--optema-primary), var(--optema-primary-dark));
|
||||
box-shadow: 0 14px 35px rgba(15, 139, 141, .26);
|
||||
}
|
||||
.btn-optema:hover, .btn-optema:focus { color:#fff; transform: translateY(-1px); box-shadow: 0 18px 44px rgba(15,139,141,.32); }
|
||||
.btn-outline-optema {
|
||||
border: 1px solid rgba(15,139,141,.3);
|
||||
color: var(--optema-primary-dark);
|
||||
background: rgba(255,255,255,.68);
|
||||
font-weight: 800;
|
||||
border-radius: 999px;
|
||||
padding: .9rem 1.35rem;
|
||||
}
|
||||
.btn-outline-optema:hover, .btn-outline-optema:focus { background: var(--optema-mint); color: var(--optema-primary-dark); }
|
||||
.trust-strip { display: flex; flex-wrap: wrap; gap: .65rem; }
|
||||
.trust-strip span, .score-pill, .status-dot {
|
||||
display: inline-flex; align-items: center; gap: .45rem;
|
||||
border: 1px solid rgba(15,139,141,.16);
|
||||
background: rgba(255,255,255,.72);
|
||||
border-radius: 999px;
|
||||
padding: .55rem .8rem;
|
||||
color: var(--optema-muted);
|
||||
font-weight: 800;
|
||||
font-size: .86rem;
|
||||
}
|
||||
.status-dot i { width: .55rem; height: .55rem; border-radius: 999px; background: #23c483; box-shadow: 0 0 0 6px rgba(35,196,131,.15); }
|
||||
.glass-card, .form-shell, .analysis-card, .empty-state, .case-card, .metric-card {
|
||||
border: 1px solid rgba(220, 232, 238, .9);
|
||||
background: rgba(255,255,255,.82);
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: var(--optema-shadow);
|
||||
border-radius: 32px;
|
||||
}
|
||||
.decision-panel { padding: clamp(1.35rem, 3vw, 2rem); }
|
||||
.mini-chart { display: grid; gap: 1rem; }
|
||||
.mini-chart div {
|
||||
--bar: 70%;
|
||||
position: relative; overflow: hidden;
|
||||
display: flex; justify-content: space-between;
|
||||
padding: .9rem 1rem;
|
||||
border-radius: 18px;
|
||||
background: #eef6f7;
|
||||
font-weight: 800;
|
||||
}
|
||||
.mini-chart div::before {
|
||||
content: ''; position: absolute; inset: 0 auto 0 0; width: var(--bar);
|
||||
background: linear-gradient(90deg, rgba(15,139,141,.24), rgba(246,174,45,.28));
|
||||
}
|
||||
.mini-chart span, .mini-chart b { position: relative; z-index: 1; }
|
||||
.bar-95 { --bar: 95% !important; } .bar-88 { --bar: 88% !important; } .bar-82 { --bar: 82% !important; }
|
||||
.solution-preview {
|
||||
padding: 1.2rem; border-radius: 24px;
|
||||
background: linear-gradient(135deg, var(--optema-ink), #183b56); color: white;
|
||||
}
|
||||
.solution-preview h2 { font-size: 1.35rem; }
|
||||
.solution-preview p { color: rgba(255,255,255,.74); margin-bottom: 0; }
|
||||
.small-label { color: var(--optema-secondary) !important; text-transform: uppercase; font-weight: 800; letter-spacing: .12em; font-size: .75rem; }
|
||||
|
||||
.section-pad { padding: clamp(4rem, 8vw, 7rem) 0; }
|
||||
.section-title, .page-title { font-size: clamp(2rem, 3.5vw, 3.6rem); font-weight: 800; line-height: 1.02; }
|
||||
.form-shell { padding: clamp(1.25rem, 3vw, 2rem); }
|
||||
.form-control, .form-select { border: 1px solid var(--optema-line); border-radius: 18px; padding: .9rem 1rem; }
|
||||
.form-control:focus, .form-select:focus { border-color: var(--optema-primary); box-shadow: 0 0 0 .22rem rgba(15,139,141,.13); }
|
||||
.form-label { font-weight: 800; color: var(--optema-ink); }
|
||||
.form-text { color: var(--optema-muted); }
|
||||
.feature-list { display: grid; gap: .85rem; margin-top: 1.5rem; }
|
||||
.feature-list div { display: flex; gap: .8rem; align-items: center; color: var(--optema-muted); }
|
||||
.feature-list strong {
|
||||
display:grid; place-items:center; width:2rem; height:2rem; border-radius:12px;
|
||||
background: var(--optema-sand); color: var(--optema-accent);
|
||||
}
|
||||
.case-card {
|
||||
display: flex; flex-direction: column; gap: .9rem;
|
||||
padding: 1.35rem; text-decoration: none; transition: .2s ease;
|
||||
}
|
||||
.case-card:hover, .case-card:focus { transform: translateY(-4px); border-color: rgba(15,139,141,.35); color: var(--optema-ink); }
|
||||
.case-card h2, .case-card h3 { font-size: 1.15rem; font-weight: 800; margin: 0; }
|
||||
.case-card p { color: var(--optema-muted); margin: 0; line-height: 1.65; }
|
||||
.case-area { color: var(--optema-primary-dark); font-size: .78rem; font-weight: 900; text-transform: uppercase; letter-spacing: .1em; }
|
||||
.case-meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .7rem; color: var(--optema-muted); font-weight: 800; font-size: .86rem; margin-top: auto; }
|
||||
.empty-state { padding: 2rem; text-align: center; }
|
||||
.empty-state p { color: var(--optema-muted); }
|
||||
.link-optema { color: var(--optema-primary-dark); font-weight: 900; text-decoration: none; }
|
||||
.link-optema:hover { color: var(--optema-accent); }
|
||||
.site-footer {
|
||||
padding: 1.5rem 0; color: var(--optema-muted);
|
||||
border-top: 1px solid var(--optema-line); background: rgba(255,255,255,.68);
|
||||
}
|
||||
.page-shell { min-height: 100vh; background: linear-gradient(180deg, rgba(223,248,245,.55), transparent 24rem); }
|
||||
.detail-hero { padding: clamp(3rem, 6vw, 5rem) 0; background: linear-gradient(135deg, rgba(223,248,245,.9), rgba(255,246,228,.85)); }
|
||||
.metric-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1rem; }
|
||||
.metric-card { padding: 1.25rem; }
|
||||
.metric-card span { color: var(--optema-muted); font-weight: 800; display:block; }
|
||||
.metric-card strong { font-family:'Plus Jakarta Sans'; font-size: 1.75rem; }
|
||||
.analysis-card { padding: clamp(1.2rem, 2.6vw, 2rem); }
|
||||
.analysis-card h2 { font-weight: 800; margin-bottom: 1.2rem; }
|
||||
.cause-list { display: grid; gap: 1.2rem; }
|
||||
.cause-list article p { color: var(--optema-muted); margin: .6rem 0 0; line-height: 1.6; }
|
||||
.progress { height: .65rem; background: #e6f2f3; border-radius: 999px; margin-top: .55rem; }
|
||||
.progress-bar { background: linear-gradient(90deg, var(--optema-primary), var(--optema-secondary)); }
|
||||
.solution-table { --bs-table-bg: transparent; }
|
||||
.solution-table th { color: var(--optema-muted); text-transform: uppercase; letter-spacing: .08em; font-size: .78rem; }
|
||||
.solution-table td { padding: 1rem .75rem; }
|
||||
.solution-table p { color: var(--optema-muted); margin: .25rem 0; }
|
||||
.solution-table small { color: var(--optema-primary-dark); font-weight: 800; }
|
||||
.rank-badge { display: inline-flex; border-radius: 999px; background: var(--optema-sand); color: var(--optema-accent); padding: .35rem .6rem; font-weight: 900; }
|
||||
.action-card { background: linear-gradient(145deg, rgba(255,255,255,.92), rgba(223,248,245,.76)); }
|
||||
.timeline { display: grid; gap: 1rem; }
|
||||
.timeline article { display: grid; grid-template-columns: 5rem 1fr; gap: 1rem; align-items: start; }
|
||||
.timeline article > span {
|
||||
display: inline-flex; justify-content:center; padding: .45rem .65rem; border-radius: 999px;
|
||||
background: var(--optema-ink); color: white; font-weight: 900; font-size: .82rem;
|
||||
}
|
||||
.timeline h3 { font-size: 1.05rem; font-weight: 800; margin-bottom: .2rem; }
|
||||
.timeline p { color: var(--optema-muted); margin: 0; line-height: 1.6; }
|
||||
|
||||
.score-width-0 { width: 0%; }
|
||||
.score-width-1 { width: 1%; }
|
||||
.score-width-2 { width: 2%; }
|
||||
.score-width-3 { width: 3%; }
|
||||
.score-width-4 { width: 4%; }
|
||||
.score-width-5 { width: 5%; }
|
||||
.score-width-6 { width: 6%; }
|
||||
.score-width-7 { width: 7%; }
|
||||
.score-width-8 { width: 8%; }
|
||||
.score-width-9 { width: 9%; }
|
||||
.score-width-10 { width: 10%; }
|
||||
.score-width-11 { width: 11%; }
|
||||
.score-width-12 { width: 12%; }
|
||||
.score-width-13 { width: 13%; }
|
||||
.score-width-14 { width: 14%; }
|
||||
.score-width-15 { width: 15%; }
|
||||
.score-width-16 { width: 16%; }
|
||||
.score-width-17 { width: 17%; }
|
||||
.score-width-18 { width: 18%; }
|
||||
.score-width-19 { width: 19%; }
|
||||
.score-width-20 { width: 20%; }
|
||||
.score-width-21 { width: 21%; }
|
||||
.score-width-22 { width: 22%; }
|
||||
.score-width-23 { width: 23%; }
|
||||
.score-width-24 { width: 24%; }
|
||||
.score-width-25 { width: 25%; }
|
||||
.score-width-26 { width: 26%; }
|
||||
.score-width-27 { width: 27%; }
|
||||
.score-width-28 { width: 28%; }
|
||||
.score-width-29 { width: 29%; }
|
||||
.score-width-30 { width: 30%; }
|
||||
.score-width-31 { width: 31%; }
|
||||
.score-width-32 { width: 32%; }
|
||||
.score-width-33 { width: 33%; }
|
||||
.score-width-34 { width: 34%; }
|
||||
.score-width-35 { width: 35%; }
|
||||
.score-width-36 { width: 36%; }
|
||||
.score-width-37 { width: 37%; }
|
||||
.score-width-38 { width: 38%; }
|
||||
.score-width-39 { width: 39%; }
|
||||
.score-width-40 { width: 40%; }
|
||||
.score-width-41 { width: 41%; }
|
||||
.score-width-42 { width: 42%; }
|
||||
.score-width-43 { width: 43%; }
|
||||
.score-width-44 { width: 44%; }
|
||||
.score-width-45 { width: 45%; }
|
||||
.score-width-46 { width: 46%; }
|
||||
.score-width-47 { width: 47%; }
|
||||
.score-width-48 { width: 48%; }
|
||||
.score-width-49 { width: 49%; }
|
||||
.score-width-50 { width: 50%; }
|
||||
.score-width-51 { width: 51%; }
|
||||
.score-width-52 { width: 52%; }
|
||||
.score-width-53 { width: 53%; }
|
||||
.score-width-54 { width: 54%; }
|
||||
.score-width-55 { width: 55%; }
|
||||
.score-width-56 { width: 56%; }
|
||||
.score-width-57 { width: 57%; }
|
||||
.score-width-58 { width: 58%; }
|
||||
.score-width-59 { width: 59%; }
|
||||
.score-width-60 { width: 60%; }
|
||||
.score-width-61 { width: 61%; }
|
||||
.score-width-62 { width: 62%; }
|
||||
.score-width-63 { width: 63%; }
|
||||
.score-width-64 { width: 64%; }
|
||||
.score-width-65 { width: 65%; }
|
||||
.score-width-66 { width: 66%; }
|
||||
.score-width-67 { width: 67%; }
|
||||
.score-width-68 { width: 68%; }
|
||||
.score-width-69 { width: 69%; }
|
||||
.score-width-70 { width: 70%; }
|
||||
.score-width-71 { width: 71%; }
|
||||
.score-width-72 { width: 72%; }
|
||||
.score-width-73 { width: 73%; }
|
||||
.score-width-74 { width: 74%; }
|
||||
.score-width-75 { width: 75%; }
|
||||
.score-width-76 { width: 76%; }
|
||||
.score-width-77 { width: 77%; }
|
||||
.score-width-78 { width: 78%; }
|
||||
.score-width-79 { width: 79%; }
|
||||
.score-width-80 { width: 80%; }
|
||||
.score-width-81 { width: 81%; }
|
||||
.score-width-82 { width: 82%; }
|
||||
.score-width-83 { width: 83%; }
|
||||
.score-width-84 { width: 84%; }
|
||||
.score-width-85 { width: 85%; }
|
||||
.score-width-86 { width: 86%; }
|
||||
.score-width-87 { width: 87%; }
|
||||
.score-width-88 { width: 88%; }
|
||||
.score-width-89 { width: 89%; }
|
||||
.score-width-90 { width: 90%; }
|
||||
.score-width-91 { width: 91%; }
|
||||
.score-width-92 { width: 92%; }
|
||||
.score-width-93 { width: 93%; }
|
||||
.score-width-94 { width: 94%; }
|
||||
.score-width-95 { width: 95%; }
|
||||
.score-width-96 { width: 96%; }
|
||||
.score-width-97 { width: 97%; }
|
||||
.score-width-98 { width: 98%; }
|
||||
.score-width-99 { width: 99%; }
|
||||
.score-width-100 { width: 100%; }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.metric-grid { grid-template-columns: 1fr; }
|
||||
.timeline article { grid-template-columns: 1fr; }
|
||||
.hero-section { min-height: auto; }
|
||||
}
|
||||
|
||||
|
||||
/* Streamlit-inspired MVP workspace */
|
||||
.hero-subtitle {
|
||||
font-family: 'Plus Jakarta Sans', 'Inter', sans-serif;
|
||||
font-size: clamp(1.25rem, 1.2vw + 1rem, 2rem);
|
||||
font-weight: 800;
|
||||
color: var(--optema-primary-dark);
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.mvp-hero .display-heading { letter-spacing: -0.07em; }
|
||||
.mvp-workspace .sticky-lg-top { top: 6rem; }
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
.panel-heading h2 {
|
||||
font-size: 1.45rem;
|
||||
margin: 0 0 .25rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
.panel-heading p,
|
||||
.form-note,
|
||||
.formula-note,
|
||||
.detail-status-card p {
|
||||
color: var(--optema-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.panel-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, var(--optema-mint), var(--optema-sand));
|
||||
box-shadow: 0 16px 28px rgba(15, 139, 141, .12);
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
.problem-textarea {
|
||||
min-height: 15rem;
|
||||
resize: vertical;
|
||||
border-radius: 22px;
|
||||
border-color: rgba(15,139,141,.18);
|
||||
background: rgba(255,255,255,.82);
|
||||
}
|
||||
.problem-textarea:focus {
|
||||
border-color: var(--optema-primary);
|
||||
box-shadow: 0 0 0 .25rem rgba(15,139,141,.12);
|
||||
}
|
||||
.urgency-slider { accent-color: var(--optema-primary); }
|
||||
.urgency-value {
|
||||
min-width: 2.4rem;
|
||||
text-align: center;
|
||||
padding: .35rem .7rem;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
background: var(--optema-ink);
|
||||
font-weight: 900;
|
||||
}
|
||||
.range-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--optema-muted);
|
||||
font-size: .82rem;
|
||||
font-weight: 800;
|
||||
margin-top: -.3rem;
|
||||
}
|
||||
.output-panel { overflow: hidden; }
|
||||
.output-success {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
color: #087447;
|
||||
background: rgba(35, 196, 131, .12);
|
||||
border: 1px solid rgba(35, 196, 131, .22);
|
||||
border-radius: 999px;
|
||||
padding: .7rem 1rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
.output-section {
|
||||
padding: 1.25rem 0;
|
||||
border-top: 1px solid var(--optema-line);
|
||||
}
|
||||
.output-section:first-of-type { border-top: 0; padding-top: 0; }
|
||||
.output-section h2 { font-size: clamp(1.25rem, 1vw + 1rem, 1.8rem); }
|
||||
.compact-cause-list { gap: .85rem; }
|
||||
.compact-cause-list article p { display: none; }
|
||||
.preview-table th,
|
||||
.preview-table td { white-space: nowrap; }
|
||||
.detail-status-card { padding: 1.35rem; }
|
||||
.bar-85 { --bar: 85% !important; }
|
||||
.bar-70 { --bar: 70% !important; }
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.mvp-workspace .sticky-lg-top { position: static; }
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user