adaptive-memory-multi-model-router 2.15.4 → 2.15.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/adapters-ci.yml +142 -0
- package/.github/workflows/ci.yml +2 -5
- package/.github/workflows/pypi-publish.yml +102 -0
- package/AGENT_COUNCIL_FINDINGS.md +123 -98
- package/CAMPAIGN_SUMMARY.md +87 -0
- package/CHANGELOG.md +7 -1
- package/ENTERPRISE_INTEGRATIONS.md +94 -0
- package/INTEGRATION_PROGRESS.md +96 -0
- package/OPPORTUNITIES_100.md +277 -0
- package/POPULARITY_BOOSTERS.md +1 -1
- package/README.md +137 -142
- package/README_ja.md +17 -21
- package/README_zh.md +13 -17
- package/TODO_BROWSER_AUTOMATION.md +87 -0
- package/a3m-integrations-summary.md +73 -0
- package/adapters/a3m_adapter/__init__.py +39 -3
- package/adapters/a3m_adapter/adapter/__init__.py +16 -1
- package/adapters/a3m_adapter/adapter/autogen.py +169 -0
- package/adapters/a3m_adapter/adapter/haystack.py +197 -0
- package/adapters/a3m_adapter/adapter/langgraph.py +196 -0
- package/adapters/a3m_adapter/adapter/pinecone.py +217 -0
- package/adapters/a3m_adapter/adapter/vercel.py +188 -0
- package/adapters/a3m_adapter/tests/test_adapters.py +29 -31
- package/adapters/a3m_adapter/tests/test_integration.py +80 -0
- package/adapters/requirements-dev.txt +6 -0
- package/adapters/requirements.txt +4 -0
- package/apps/cost-calculator/README.md +72 -0
- package/apps/cost-calculator/calculator.css +280 -0
- package/apps/cost-calculator/calculator.js +150 -0
- package/apps/cost-calculator/index.html +321 -0
- package/apps/cost-calculator/package.json +13 -0
- package/articles/ANNOUNCEMENT_reddit_ml.md +76 -0
- package/articles/ANNOUNCEMENT_vc/347/244/276/345/214/272.md +71 -0
- package/articles/ANNOUNCEMENT_vercel.md +85 -0
- package/articles/CHINESE_DIRECTORIES.md +2 -2
- package/articles/CHINESE_SUBMISSIONS_READY.md +5 -5
- package/articles/HN_POST_READY.md +16 -20
- package/articles/INDIEHACKERS_READY.md +19 -34
- package/demo.py +251 -0
- package/dist/analytics/costAnalytics.d.ts +0 -1
- package/dist/benchmark/reproducible.d.ts +0 -1
- package/dist/cache/semanticCache.d.ts +0 -1
- package/dist/cost/budgetEnforcer.d.ts +0 -1
- package/dist/cost/costTracker.d.ts +0 -1
- package/dist/ensemble/multiRoundDialog.d.ts +0 -1
- package/dist/ensemble/shapleyValue.d.ts +0 -1
- package/dist/ensemble.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/integrations/oauth.d.ts +0 -1
- package/dist/integrations/scienceAdapter.d.ts +0 -1
- package/dist/memory/autoFetch.d.ts +0 -1
- package/dist/memory/hybridMemory.d.ts +0 -1
- package/dist/memory/memoryTree.d.ts +0 -1
- package/dist/memory/obsidianVault.d.ts +0 -1
- package/dist/memory/reasoningBank.d.ts +0 -1
- package/dist/observability/changeWatch.d.ts +0 -1
- package/dist/observability/fatigueDetector.d.ts +0 -1
- package/dist/observability/index.d.ts +0 -1
- package/dist/observability/metrics.d.ts +0 -1
- package/dist/observability/middleware.d.ts +0 -1
- package/dist/observability/tracer.d.ts +0 -1
- package/dist/observability/types.d.ts +0 -1
- package/dist/providers/providerConfig.d.ts +5 -2
- package/dist/providers/providerConfig.js +1006 -1
- package/dist/providers/providerConfig.js.map +1 -1
- package/dist/routing/advancedRouter.d.ts +1 -2
- package/dist/routing/advancedRouter.js +84 -0
- package/dist/routing/advancedRouter.js.map +1 -1
- package/dist/routing/crossModelValidation.d.ts +0 -1
- package/dist/routing/providerHealth.d.ts +77 -2
- package/dist/routing/providerHealth.js +190 -4
- package/dist/routing/providerHealth.js.map +1 -1
- package/dist/routing/providerRetry.d.ts +0 -1
- package/dist/routing/shadowSampler.d.ts +187 -0
- package/dist/routing/shadowSampler.js +306 -0
- package/dist/routing/shadowSampler.js.map +1 -0
- package/dist/sdk.d.ts +0 -1
- package/dist/security/guardrails.d.ts +0 -1
- package/dist/server/dashboard.d.ts +0 -1
- package/dist/server/handlers/chatHandler.d.ts +0 -1
- package/dist/server/handlers/completionsHandler.d.ts +0 -1
- package/dist/server/handlers/embeddingsHandler.d.ts +0 -1
- package/dist/server/handlers/healthHandler.d.ts +0 -1
- package/dist/server/handlers/metricsHandler.d.ts +0 -1
- package/dist/server/handlers/modelsHandler.d.ts +0 -1
- package/dist/server/metrics.d.ts +0 -1
- package/dist/server/modelMapper.d.ts +0 -1
- package/dist/server/proxyServer.d.ts +0 -1
- package/dist/server/router.d.ts +0 -1
- package/dist/server/state.d.ts +0 -1
- package/dist/tui/dashboard.d.ts +0 -1
- package/dist/utils/costUtils.d.ts +0 -1
- package/dist/utils/sorting.d.ts +0 -1
- package/dist/utils/tokenUtils.d.ts +0 -1
- package/docker-compose.yml +84 -60
- package/docs/comparison.md +2 -2
- package/docs/llms-full.txt +89 -0
- package/docs/llms.txt +25 -67
- package/docs-site/index.html +9 -9
- package/integrations/browser-use/README.md +155 -0
- package/integrations/browser-use/examples/form-filling.ts +179 -0
- package/integrations/browser-use/index.ts +145 -0
- package/integrations/browser-use/package.json +34 -0
- package/integrations/browser-use/tsconfig.json +17 -0
- package/integrations/mcp-browser/README.md +177 -0
- package/integrations/mcp-browser/package.json +38 -0
- package/integrations/mcp-browser/src/index.ts +297 -0
- package/integrations/sota-browser/README.md +191 -0
- package/integrations/sota-browser/package.json +34 -0
- package/llms-full.txt +6 -6
- package/llms.txt +25 -67
- package/package.json +34 -5
- package/packages/a3m-vercel-ai/README.md +161 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts +12 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts.map +1 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js +289 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js.map +1 -0
- package/packages/a3m-vercel-ai/dist/index.d.ts +82 -0
- package/packages/a3m-vercel-ai/dist/index.d.ts.map +1 -0
- package/packages/a3m-vercel-ai/dist/index.js +79 -0
- package/packages/a3m-vercel-ai/dist/index.js.map +1 -0
- package/packages/a3m-vercel-ai/dist/types.d.ts +97 -0
- package/packages/a3m-vercel-ai/dist/types.d.ts.map +1 -0
- package/packages/a3m-vercel-ai/dist/types.js +5 -0
- package/packages/a3m-vercel-ai/dist/types.js.map +1 -0
- package/packages/a3m-vercel-ai/package-lock.json +969 -0
- package/packages/a3m-vercel-ai/package.json +46 -0
- package/packages/a3m-vercel-ai/src/a3m-language-model.ts +381 -0
- package/packages/a3m-vercel-ai/src/index.ts +104 -0
- package/packages/a3m-vercel-ai/src/types.ts +116 -0
- package/packages/a3m-vercel-ai/tsconfig.json +20 -0
- package/packages/agentkit-adapter/LICENSE +21 -0
- package/packages/agentkit-adapter/README.md +126 -0
- package/packages/agentkit-adapter/examples/agentkit-example.ts +139 -0
- package/packages/agentkit-adapter/package.json +57 -0
- package/packages/agentkit-adapter/src/adapter.ts +381 -0
- package/packages/agentkit-adapter/src/index.ts +36 -0
- package/packages/agentkit-adapter/src/types.ts +105 -0
- package/packages/agentkit-adapter/src/util.ts +13 -0
- package/packages/agentkit-adapter/tsconfig.json +22 -0
- package/prometheus.yml +8 -0
- package/python/README.md +92 -65
- package/python/a3m/__init__.py +32 -3
- package/python/a3m/adapters/__init__.py +21 -0
- package/python/a3m/adapters/langchain.py +190 -0
- package/python/a3m/adapters/llamaindex.py +249 -0
- package/python/a3m/adapters/qdrant.py +240 -0
- package/python/a3m/adapters/weaviate.py +263 -0
- package/python/a3m/client.py +5 -0
- package/python/a3m_router.egg-info/PKG-INFO +172 -0
- package/python/a3m_router.egg-info/SOURCES.txt +17 -0
- package/python/a3m_router.egg-info/dependency_links.txt +1 -0
- package/python/a3m_router.egg-info/requires.txt +24 -0
- package/python/a3m_router.egg-info/top_level.txt +1 -0
- package/python/build_verify.sh +32 -0
- package/python/dist/a3m_router-2.2.1-py3-none-any.whl +0 -0
- package/python/dist/a3m_router-2.2.1.tar.gz +0 -0
- package/python/dist/a3m_router-2.2.2-py3-none-any.whl +0 -0
- package/python/dist/a3m_router-2.2.2.tar.gz +0 -0
- package/python/mcp-server/README.md +172 -0
- package/python/mcp-server/a3m_mcp/__init__.py +15 -0
- package/python/mcp-server/a3m_mcp/__main__.py +15 -0
- package/python/mcp-server/a3m_mcp/server.py +339 -0
- package/python/mcp-server/pyproject.toml +24 -0
- package/python/pyproject.toml +56 -5
- package/python/setup.py +3 -28
- package/src/providers/providerConfig.ts +1053 -1
- package/summary.txt +38 -0
- package/tsconfig.build.json +1 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -94
- package/.github/ISSUE_TEMPLATE/config.yml +0 -17
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -71
- package/.well-known/ai-plugin.json +0 -16
|
@@ -18,73 +18,75 @@ sys.path.insert(0, '.')
|
|
|
18
18
|
logging.basicConfig(level=logging.INFO)
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
def test_langchain_adapter():
|
|
22
23
|
"""Test LangChain adapter."""
|
|
23
24
|
print("Testing LangChain adapter...")
|
|
24
25
|
|
|
25
26
|
try:
|
|
26
|
-
from
|
|
27
|
+
from a3m_adapter import A3MLangChainAdapter
|
|
27
28
|
|
|
28
29
|
# Initialize
|
|
29
|
-
llm =
|
|
30
|
-
print(f"
|
|
30
|
+
llm = A3MLangChainAdapter(model="auto", temperature=0.7)
|
|
31
|
+
print(f"✅ Initialized: {llm}")
|
|
31
32
|
|
|
32
33
|
# Test simple generation
|
|
33
34
|
# Note: This would make actual API calls - we'll skip for now
|
|
34
35
|
# In a real test, we'd mock the A3M router
|
|
35
|
-
print("
|
|
36
|
-
|
|
36
|
+
print("✅ LangChain adapter structure OK")
|
|
37
|
+
assert llm is not None
|
|
37
38
|
|
|
38
39
|
except Exception as e:
|
|
39
|
-
print(f"
|
|
40
|
-
|
|
40
|
+
print(f"❌ LangChain adapter failed: {e}")
|
|
41
|
+
raise
|
|
42
|
+
|
|
41
43
|
|
|
42
44
|
def test_llamaindex_adapter():
|
|
43
45
|
"""Test LlamaIndex adapter."""
|
|
44
46
|
print("Testing LlamaIndex adapter...")
|
|
45
47
|
|
|
46
48
|
try:
|
|
47
|
-
from
|
|
49
|
+
from a3m_adapter import A3MLlamaIndexAdapter
|
|
48
50
|
|
|
49
51
|
# Initialize
|
|
50
|
-
llm =
|
|
51
|
-
print(f"
|
|
52
|
+
llm = A3MLlamaIndexAdapter(model="auto", temperature=0.5)
|
|
53
|
+
print(f"✅ Initialized: {llm}")
|
|
52
54
|
|
|
53
55
|
# Check metadata
|
|
54
56
|
metadata = llm.metadata
|
|
55
|
-
print(f"
|
|
56
|
-
|
|
57
|
+
print(f"✅ Metadata: {metadata}")
|
|
58
|
+
assert metadata is not None
|
|
57
59
|
|
|
58
60
|
except Exception as e:
|
|
59
|
-
print(f"
|
|
60
|
-
|
|
61
|
+
print(f"❌ LlamaIndex adapter failed: {e}")
|
|
62
|
+
raise
|
|
63
|
+
|
|
61
64
|
|
|
62
65
|
def test_config():
|
|
63
66
|
"""Test configuration."""
|
|
64
67
|
print("Testing configuration...")
|
|
65
68
|
|
|
66
69
|
try:
|
|
67
|
-
from
|
|
70
|
+
from a3m_adapter import A3MConfig
|
|
68
71
|
|
|
69
72
|
# Test defaults
|
|
70
73
|
config = A3MConfig()
|
|
71
|
-
print(f"
|
|
74
|
+
print(f"✅ Default config: model={config.model}")
|
|
72
75
|
|
|
73
76
|
# Test to_dict
|
|
74
77
|
data = config.to_dict()
|
|
75
78
|
assert 'model' in data
|
|
76
|
-
print("
|
|
79
|
+
print("✅ Config to_dict works")
|
|
77
80
|
|
|
78
81
|
# Test JSON serialization
|
|
79
82
|
json_str = config.to_json()
|
|
80
83
|
assert '"model"' in json_str
|
|
81
|
-
print("
|
|
82
|
-
|
|
83
|
-
return True
|
|
84
|
+
print("✅ Config JSON serialization works")
|
|
84
85
|
|
|
85
86
|
except Exception as e:
|
|
86
|
-
print(f"
|
|
87
|
-
|
|
87
|
+
print(f"❌ Config test failed: {e}")
|
|
88
|
+
raise
|
|
89
|
+
|
|
88
90
|
|
|
89
91
|
def main():
|
|
90
92
|
"""Run all tests."""
|
|
@@ -102,19 +104,15 @@ def main():
|
|
|
102
104
|
total = len(tests)
|
|
103
105
|
|
|
104
106
|
for test in tests:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
test()
|
|
108
|
+
passed += 1
|
|
107
109
|
print()
|
|
108
110
|
|
|
109
111
|
print("=" * 50)
|
|
110
112
|
print(f"Results: {passed}/{total} tests passed")
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return 0
|
|
115
|
-
else:
|
|
116
|
-
print("��❌ Some tests failed")
|
|
117
|
-
return 1
|
|
113
|
+
print("🎉 All tests passed!")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
118
116
|
|
|
119
117
|
if __name__ == "__main__":
|
|
120
118
|
sys.exit(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for A3M Router adapters.
|
|
3
|
+
Requires A3M Router server running on localhost:8787
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def a3m_server_url():
|
|
12
|
+
"""Get A3M Router server URL."""
|
|
13
|
+
return os.environ.get("A3M_SERVER_URL", "http://localhost:8787")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def skip_if_no_server():
|
|
18
|
+
"""Skip test if server is not available."""
|
|
19
|
+
import requests
|
|
20
|
+
try:
|
|
21
|
+
resp = requests.get("http://localhost:8787/health", timeout=2)
|
|
22
|
+
if resp.status_code != 200:
|
|
23
|
+
pytest.skip("A3M Router server not running")
|
|
24
|
+
except:
|
|
25
|
+
pytest.skip("A3M Router server not running")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.integration
|
|
29
|
+
def test_simple_chat_completion(a3m_server_url, skip_if_no_server):
|
|
30
|
+
"""Test simple chat completion via HTTP API."""
|
|
31
|
+
import requests
|
|
32
|
+
|
|
33
|
+
response = requests.post(
|
|
34
|
+
f"{a3m_server_url}/v1/chat/completions",
|
|
35
|
+
json={
|
|
36
|
+
"model": "auto",
|
|
37
|
+
"messages": [{"role": "user", "content": "What is 2+2?"}]
|
|
38
|
+
},
|
|
39
|
+
timeout=30
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert response.status_code == 200
|
|
43
|
+
data = response.json()
|
|
44
|
+
assert "choices" in data
|
|
45
|
+
assert len(data["choices"]) > 0
|
|
46
|
+
assert "message" in data["choices"][0]
|
|
47
|
+
assert data["choices"][0]["message"]["content"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.integration
|
|
51
|
+
def test_parallel_ensemble(a3m_server_url, skip_if_no_server):
|
|
52
|
+
"""Test parallel ensemble with multiple providers."""
|
|
53
|
+
import requests
|
|
54
|
+
|
|
55
|
+
response = requests.post(
|
|
56
|
+
f"{a3m_server_url}/v1/chat/completions",
|
|
57
|
+
json={
|
|
58
|
+
"model": "auto",
|
|
59
|
+
"messages": [{"role": "user", "content": "Explain gravity"}],
|
|
60
|
+
"parallel_ensemble": 3,
|
|
61
|
+
},
|
|
62
|
+
timeout=60
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
assert response.status_code == 200
|
|
66
|
+
data = response.json()
|
|
67
|
+
assert "choices" in data
|
|
68
|
+
assert "provider" in data
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.integration
|
|
72
|
+
def test_health_endpoint(a3m_server_url, skip_if_no_server):
|
|
73
|
+
"""Test health endpoint."""
|
|
74
|
+
import requests
|
|
75
|
+
|
|
76
|
+
response = requests.get(f"{a3m_server_url}/health", timeout=10)
|
|
77
|
+
|
|
78
|
+
assert response.status_code == 200
|
|
79
|
+
data = response.json()
|
|
80
|
+
assert "providers" in data or "status" in data
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# A3M Router Cost Calculator
|
|
2
|
+
|
|
3
|
+
**Live:** https://a3m.router/calculator
|
|
4
|
+
|
|
5
|
+
Drop-in interactive calculator. No build step. No npm dependencies for the calculator itself.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
- Input: monthly request volume + model mix
|
|
10
|
+
- Output: side-by-side cost comparison (baseline vs A3M routed)
|
|
11
|
+
- Generates shareable URL with encoded config (base64url)
|
|
12
|
+
- Exports comparison as CSV
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Open directly
|
|
18
|
+
open apps/cost-calculator/index.html
|
|
19
|
+
|
|
20
|
+
# Or serve locally
|
|
21
|
+
npx serve apps/cost-calculator
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Embed in any site
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<iframe
|
|
28
|
+
src="https://a3m.router/calculator"
|
|
29
|
+
width="100%"
|
|
30
|
+
height="700"
|
|
31
|
+
frameborder="0"
|
|
32
|
+
style="border-radius: 12px;"
|
|
33
|
+
></iframe>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
calculator.js — Pure JS, no dependencies. All pricing, routing, and encoding logic.
|
|
40
|
+
calculator.css — Standalone styles. Dark theme, responsive.
|
|
41
|
+
index.html — Self-contained. Vanilla JS. Loads calculator.js.
|
|
42
|
+
package.json — vitest for unit testing the pure JS logic.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Testing
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd apps/cost-calculator
|
|
49
|
+
npm install
|
|
50
|
+
npm test
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Models Supported
|
|
54
|
+
|
|
55
|
+
| Premium Model | Routed To | Savings |
|
|
56
|
+
|---------------|-----------|---------|
|
|
57
|
+
| GPT-4o | GPT-4o-mini | ~35% |
|
|
58
|
+
| Claude 3.5 Sonnet | Claude 3 Haiku | ~40% |
|
|
59
|
+
| Gemini 1.5 Pro | Gemini 1.5 Flash | ~45% |
|
|
60
|
+
| Groq Llama 3.3 70B | Groq Mixtral 8x7B | ~30% |
|
|
61
|
+
|
|
62
|
+
## Pricing Source
|
|
63
|
+
|
|
64
|
+
OpenRouter provider rates (per 1M tokens). Update `PRICING` constant in `calculator.js` to reflect latest rates.
|
|
65
|
+
|
|
66
|
+
## Next Steps
|
|
67
|
+
|
|
68
|
+
- [ ] Deploy to `a3m.router/calculator` (Vercel static)
|
|
69
|
+
- [ ] Add UTM tracking on share links
|
|
70
|
+
- [ ] Add "embed widget" script for dynamic resize
|
|
71
|
+
- [ ] Write HN + Reddit post with actual example calculations
|
|
72
|
+
- [ ] Add unit tests with vitest
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/* A3M Router Cost Calculator Styles */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--a3m-primary: #6366f1;
|
|
5
|
+
--a3m-accent: #818cf8;
|
|
6
|
+
--a3m-bg: #0f0f1a;
|
|
7
|
+
--a3m-surface: #1a1a2e;
|
|
8
|
+
--a3m-border: #2d2d4a;
|
|
9
|
+
--a3m-text: #e2e8f0;
|
|
10
|
+
--a3m-muted: #94a3b8;
|
|
11
|
+
--a3m-green: #22c55e;
|
|
12
|
+
--a3m-red: #ef4444;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
19
|
+
background: var(--a3m-bg);
|
|
20
|
+
color: var(--a3m-text);
|
|
21
|
+
min-height: 100vh;
|
|
22
|
+
padding: 2rem 1rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.container {
|
|
26
|
+
max-width: 900px;
|
|
27
|
+
margin: 0 auto;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
h1 {
|
|
31
|
+
font-size: 1.75rem;
|
|
32
|
+
font-weight: 700;
|
|
33
|
+
text-align: center;
|
|
34
|
+
margin-bottom: 0.5rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.subtitle {
|
|
38
|
+
text-align: center;
|
|
39
|
+
color: var(--a3m-muted);
|
|
40
|
+
margin-bottom: 2rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.card {
|
|
44
|
+
background: var(--a3m-surface);
|
|
45
|
+
border: 1px solid var(--a3m-border);
|
|
46
|
+
border-radius: 12px;
|
|
47
|
+
padding: 1.5rem;
|
|
48
|
+
margin-bottom: 1.5rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.card-title {
|
|
52
|
+
font-size: 0.875rem;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
text-transform: uppercase;
|
|
55
|
+
letter-spacing: 0.05em;
|
|
56
|
+
color: var(--a3m-muted);
|
|
57
|
+
margin-bottom: 1rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Input grid */
|
|
61
|
+
.input-grid {
|
|
62
|
+
display: grid;
|
|
63
|
+
grid-template-columns: 1fr 1fr;
|
|
64
|
+
gap: 1rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@media (max-width: 600px) {
|
|
68
|
+
.input-grid { grid-template-columns: 1fr; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.input-group {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: 0.5rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.input-group label {
|
|
78
|
+
font-size: 0.875rem;
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
color: var(--a3m-muted);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.input-group input,
|
|
84
|
+
.input-group select {
|
|
85
|
+
padding: 0.625rem 0.875rem;
|
|
86
|
+
background: var(--a3m-bg);
|
|
87
|
+
border: 1px solid var(--a3m-border);
|
|
88
|
+
border-radius: 8px;
|
|
89
|
+
color: var(--a3m-text);
|
|
90
|
+
font-size: 1rem;
|
|
91
|
+
width: 100%;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.input-group input:focus,
|
|
95
|
+
.input-group select:focus {
|
|
96
|
+
outline: none;
|
|
97
|
+
border-color: var(--a3m-primary);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Model mix rows */
|
|
101
|
+
.model-mix {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
gap: 0.75rem;
|
|
105
|
+
margin-top: 1rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.model-row {
|
|
109
|
+
display: grid;
|
|
110
|
+
grid-template-columns: 1fr 100px auto;
|
|
111
|
+
gap: 0.5rem;
|
|
112
|
+
align-items: center;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.model-row select,
|
|
116
|
+
.model-row input {
|
|
117
|
+
padding: 0.5rem;
|
|
118
|
+
background: var(--a3m-bg);
|
|
119
|
+
border: 1px solid var(--a3m-border);
|
|
120
|
+
border-radius: 6px;
|
|
121
|
+
color: var(--a3m-text);
|
|
122
|
+
font-size: 0.875rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.model-row span {
|
|
126
|
+
color: var(--a3m-muted);
|
|
127
|
+
font-size: 0.875rem;
|
|
128
|
+
white-space: nowrap;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.btn {
|
|
132
|
+
display: inline-flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: 0.5rem;
|
|
135
|
+
padding: 0.625rem 1.25rem;
|
|
136
|
+
background: var(--a3m-primary);
|
|
137
|
+
color: white;
|
|
138
|
+
border: none;
|
|
139
|
+
border-radius: 8px;
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
transition: background 0.2s;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.btn:hover { background: var(--a3m-accent); }
|
|
147
|
+
.btn-secondary {
|
|
148
|
+
background: var(--a3m-border);
|
|
149
|
+
color: var(--a3m-text);
|
|
150
|
+
}
|
|
151
|
+
.btn-secondary:hover { background: #3d3d5a; }
|
|
152
|
+
|
|
153
|
+
.btn-row {
|
|
154
|
+
display: flex;
|
|
155
|
+
gap: 0.75rem;
|
|
156
|
+
margin-top: 1.25rem;
|
|
157
|
+
flex-wrap: wrap;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Results */
|
|
161
|
+
.results-grid {
|
|
162
|
+
display: grid;
|
|
163
|
+
grid-template-columns: repeat(3, 1fr);
|
|
164
|
+
gap: 1rem;
|
|
165
|
+
margin-bottom: 1.5rem;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@media (max-width: 600px) {
|
|
169
|
+
.results-grid { grid-template-columns: 1fr; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.result-card {
|
|
173
|
+
background: var(--a3m-bg);
|
|
174
|
+
border: 1px solid var(--a3m-border);
|
|
175
|
+
border-radius: 10px;
|
|
176
|
+
padding: 1.25rem;
|
|
177
|
+
text-align: center;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.result-label {
|
|
181
|
+
font-size: 0.75rem;
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
text-transform: uppercase;
|
|
184
|
+
letter-spacing: 0.05em;
|
|
185
|
+
color: var(--a3m-muted);
|
|
186
|
+
margin-bottom: 0.5rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.result-value {
|
|
190
|
+
font-size: 1.75rem;
|
|
191
|
+
font-weight: 700;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.result-value.savings { color: var(--a3m-green); }
|
|
195
|
+
.result-value.baseline { color: var(--a3m-muted); text-decoration: line-through; }
|
|
196
|
+
|
|
197
|
+
/* Breakdown table */
|
|
198
|
+
.breakdown-table {
|
|
199
|
+
width: 100%;
|
|
200
|
+
border-collapse: collapse;
|
|
201
|
+
font-size: 0.875rem;
|
|
202
|
+
margin-top: 1rem;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.breakdown-table th {
|
|
206
|
+
text-align: left;
|
|
207
|
+
padding: 0.625rem 0.75rem;
|
|
208
|
+
color: var(--a3m-muted);
|
|
209
|
+
font-weight: 600;
|
|
210
|
+
font-size: 0.75rem;
|
|
211
|
+
text-transform: uppercase;
|
|
212
|
+
border-bottom: 1px solid var(--a3m-border);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.breakdown-table td {
|
|
216
|
+
padding: 0.625rem 0.75rem;
|
|
217
|
+
border-bottom: 1px solid var(--a3m-border);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.breakdown-table tr:last-child td { border-bottom: none; }
|
|
221
|
+
.breakdown-table .num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
222
|
+
.breakdown-table .savings-cell { color: var(--a3m-green); }
|
|
223
|
+
|
|
224
|
+
/* Share bar */
|
|
225
|
+
.share-bar {
|
|
226
|
+
display: flex;
|
|
227
|
+
gap: 0.5rem;
|
|
228
|
+
align-items: center;
|
|
229
|
+
margin-top: 1rem;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.share-bar input {
|
|
233
|
+
flex: 1;
|
|
234
|
+
padding: 0.5rem 0.75rem;
|
|
235
|
+
background: var(--a3m-bg);
|
|
236
|
+
border: 1px solid var(--a3m-border);
|
|
237
|
+
border-radius: 6px;
|
|
238
|
+
color: var(--a3m-text);
|
|
239
|
+
font-size: 0.8rem;
|
|
240
|
+
font-family: monospace;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Chart */
|
|
244
|
+
.chart-container {
|
|
245
|
+
position: relative;
|
|
246
|
+
height: 250px;
|
|
247
|
+
margin: 1.5rem 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.chart-bar {
|
|
251
|
+
position: absolute;
|
|
252
|
+
bottom: 0;
|
|
253
|
+
width: 40%;
|
|
254
|
+
border-radius: 6px 6px 0 0;
|
|
255
|
+
transition: height 0.5s ease;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.chart-bar.baseline {
|
|
259
|
+
left: 15%;
|
|
260
|
+
background: var(--a3m-muted);
|
|
261
|
+
opacity: 0.5;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.chart-bar.a3m {
|
|
265
|
+
right: 15%;
|
|
266
|
+
background: var(--a3m-primary);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* Hidden until calculated */
|
|
270
|
+
.hidden { display: none; }
|
|
271
|
+
|
|
272
|
+
/* Footer */
|
|
273
|
+
.footer {
|
|
274
|
+
text-align: center;
|
|
275
|
+
color: var(--a3m-muted);
|
|
276
|
+
font-size: 0.75rem;
|
|
277
|
+
margin-top: 2rem;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.footer a { color: var(--a3m-accent); text-decoration: none; }
|