CompactAI commited on
Commit
17ef86f
Β·
verified Β·
1 Parent(s): 0051294

Upload 8 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ models/1773449581[[:space:]](1).png filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -1,168 +1,92 @@
1
  """
2
- AIFinder API Server
3
- Serves classification and training endpoints for the frontend.
4
-
5
- Public API:
6
- POST /v1/classify β€” classify text, returns top-N provider predictions.
7
- No API key required. Rate-limited to 60 requests/minute per IP.
8
  """
9
 
10
  import os
11
  import re
12
- import sys
13
- import json
14
  import joblib
15
  import numpy as np
16
- import torch
17
- import torch.nn as nn
18
- from flask import Flask, request, jsonify, send_from_directory
19
  from flask_cors import CORS
20
  from flask_limiter import Limiter
21
  from flask_limiter.util import get_remote_address
22
 
23
  from config import MODEL_DIR
24
- from model import AIFinderNet
25
- from features import FeaturePipeline
26
 
27
- app = Flask(__name__, static_folder="static", static_url_path="")
28
  CORS(app)
29
- limiter = Limiter(get_remote_address, app=app, default_limits=[])
30
 
31
- DEFAULT_TOP_N = 5
 
 
32
 
33
- pipeline = None
34
- provider_enc = None
35
- net = None
36
- device = None
37
- checkpoint = None
38
 
39
 
40
  def load_models():
41
- global pipeline, provider_enc, net, device, checkpoint
42
-
43
- pipeline = joblib.load(os.path.join(MODEL_DIR, "feature_pipeline.joblib"))
44
- provider_enc = joblib.load(os.path.join(MODEL_DIR, "provider_enc.joblib"))
45
-
46
- checkpoint = torch.load(
47
- os.path.join(MODEL_DIR, "classifier.pt"),
48
- map_location="cpu",
49
- weights_only=True,
50
- )
51
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
52
- net = AIFinderNet(
53
- input_dim=checkpoint["input_dim"],
54
- num_providers=checkpoint["num_providers"],
55
- hidden_dim=checkpoint["hidden_dim"],
56
- embed_dim=checkpoint["embed_dim"],
57
- dropout=checkpoint["dropout"],
58
- ).to(device)
59
- net.load_state_dict(checkpoint["state_dict"], strict=False)
60
- net.eval()
61
-
62
-
63
- @app.route("/")
64
- def index():
65
- return send_from_directory("static", "index.html")
66
-
67
-
68
- @app.route("/api/providers", methods=["GET"])
69
- def get_providers():
70
- """Return list of available providers."""
71
- return jsonify({"providers": sorted(provider_enc.classes_.tolist())})
72
-
73
-
74
- @app.route("/api/classify", methods=["POST"])
75
- def classify():
76
- """Classify text and return provider predictions."""
77
- data = request.json
78
- text = data.get("text", "")
79
-
80
- if len(text) < 20:
81
- return jsonify({"error": "Text too short (minimum 20 characters)"}), 400
82
-
83
- X = pipeline.transform([text])
84
- X_t = torch.tensor(X.toarray(), dtype=torch.float32).to(device)
85
 
86
- with torch.no_grad():
87
- prov_logits = net(X_t)
88
 
89
- prov_proba = torch.softmax(prov_logits.float(), dim=1)[0].cpu().numpy()
90
-
91
- top_prov_idxs = np.argsort(prov_proba)[::-1][:5]
92
- top_providers = [
93
- {
94
- "name": provider_enc.inverse_transform([i])[0],
95
- "confidence": float(prov_proba[i] * 100),
96
- }
97
- for i in top_prov_idxs
98
- ]
99
-
100
- return jsonify(
101
- {
102
- "provider": top_providers[0]["name"],
103
- "confidence": top_providers[0]["confidence"],
104
- "top_providers": top_providers,
105
- }
106
- )
107
 
108
 
109
  def _strip_think_tags(text):
110
- """Remove <think>…</think> (and <thinking>…</thinking>) blocks from input."""
111
  text = re.sub(r"<think(?:ing)?>.*?</think(?:ing)?>", "", text, flags=re.DOTALL)
112
  return text.strip()
113
 
114
 
 
 
 
 
 
 
115
  @app.route("/v1/classify", methods=["POST"])
116
  @limiter.limit("60/minute")
117
  def v1_classify():
118
- """Public API β€” classify text and return top-N provider predictions.
119
-
120
- Request JSON:
121
- text (str): The text to classify. Any <think>/<thinking> tags will be
122
- stripped automatically before classification.
123
- top_n (int): Number of results to return (default: 5).
124
-
125
- Response JSON:
126
- provider (str): Best-matching provider name.
127
- confidence (float): Confidence % for the top provider.
128
- top_providers (list): List of {name, confidence} dicts.
129
-
130
- Rate limit: 60 requests per minute per IP. No API key required.
131
-
132
- NOTE: If the text you are classifying was produced by a model that emits
133
- <think> or <thinking> blocks, you should strip those tags BEFORE
134
- sending the text. This endpoint does it for you automatically, but
135
- doing it on your side avoids wasting bytes on the wire.
136
- """
137
  data = request.get_json(silent=True)
138
  if not data or "text" not in data:
139
  return jsonify({"error": "Request body must be JSON with a 'text' field."}), 400
140
 
141
  raw_text = data["text"]
142
  text = _strip_think_tags(raw_text)
143
- top_n = data.get("top_n", DEFAULT_TOP_N)
 
144
 
145
  if not isinstance(top_n, int) or top_n < 1:
146
  top_n = DEFAULT_TOP_N
147
 
148
  if len(text) < 20:
149
- return jsonify({"error": "Text too short (minimum 20 characters after stripping think tags)."}), 400
150
-
151
- X = pipeline.transform([text])
152
- X_t = torch.tensor(X.toarray(), dtype=torch.float32).to(device)
153
-
154
- with torch.no_grad():
155
- prov_logits = net(X_t)
156
 
157
- prov_proba = torch.softmax(prov_logits.float(), dim=1)[0].cpu().numpy()
 
158
 
159
- top_idxs = np.argsort(prov_proba)[::-1][:top_n]
160
  top_providers = [
161
- {
162
- "name": provider_enc.inverse_transform([i])[0],
163
- "confidence": round(float(prov_proba[i] * 100), 2),
164
- }
165
- for i in top_idxs
166
  ]
167
 
168
  return jsonify(
@@ -176,71 +100,80 @@ def v1_classify():
176
 
177
  @app.route("/api/correct", methods=["POST"])
178
  def correct():
179
- """Train on a corrected example."""
180
- data = request.json
181
- text = data.get("text", "")
182
- correct_provider = data.get("correct_provider", "")
183
-
184
- if not text or not correct_provider:
185
- return jsonify({"error": "Missing text or correct_provider"}), 400
186
-
187
- try:
188
- prov_idx = provider_enc.transform([correct_provider])[0]
189
- except ValueError as e:
190
- return jsonify({"error": f"Unknown provider: {e}"}), 400
191
 
192
- X = pipeline.transform([text])
193
- X_t = torch.tensor(X.toarray(), dtype=torch.float32).to(device)
194
- y_prov = torch.tensor([prov_idx], dtype=torch.long).to(device)
195
 
196
- net.train()
197
- for module in net.modules():
198
- if isinstance(module, nn.modules.batchnorm._BatchNorm):
199
- module.eval()
200
 
201
- optimizer = torch.optim.AdamW(net.parameters(), lr=1e-4, weight_decay=1e-4)
202
- optimizer.zero_grad(set_to_none=True)
 
 
203
 
204
- prov_criterion = nn.CrossEntropyLoss()
205
- prov_logits = net(X_t)
206
- loss = prov_criterion(prov_logits, y_prov)
207
- loss.backward()
208
- torch.nn.utils.clip_grad_norm_(net.parameters(), max_norm=1.0)
209
- optimizer.step()
210
 
211
- net.eval()
 
 
 
212
 
213
- checkpoint["state_dict"] = net.state_dict()
214
 
215
- return jsonify({"success": True, "loss": float(loss.item())})
216
 
217
 
218
  @app.route("/api/save", methods=["POST"])
219
  def save_model():
220
- """Save the current model state to a file for export."""
221
- global checkpoint
222
- data = request.json
223
- filename = data.get("filename", "aifinder_model.pt")
224
 
225
- save_path = os.path.join(MODEL_DIR, filename)
226
- torch.save(checkpoint, save_path)
227
 
228
- return jsonify({"success": True, "filename": filename})
 
 
 
 
 
229
 
230
 
231
  @app.route("/models/<filename>")
232
  def download_model(filename):
233
- """Download exported model file."""
 
234
  return send_from_directory(MODEL_DIR, filename)
235
 
236
 
237
  @app.route("/api/status", methods=["GET"])
238
  def status():
239
- """Check if models are loaded."""
240
  return jsonify(
241
  {
242
- "loaded": net is not None,
243
- "device": str(device) if device else None,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  }
245
  )
246
 
@@ -248,5 +181,8 @@ def status():
248
  if __name__ == "__main__":
249
  print("Loading models...")
250
  load_models()
251
- print(f"Ready on {device}")
 
 
 
252
  app.run(host="0.0.0.0", port=7860)
 
1
  """
2
+ AIFinder Flask API
3
+ Serves the trained sklearn ensemble via the AIFinder inference class.
 
 
 
 
4
  """
5
 
6
  import os
7
  import re
8
+
 
9
  import joblib
10
  import numpy as np
11
+ from sklearn.ensemble import RandomForestClassifier
12
+ from flask import Flask, jsonify, request, send_from_directory, render_template
 
13
  from flask_cors import CORS
14
  from flask_limiter import Limiter
15
  from flask_limiter.util import get_remote_address
16
 
17
  from config import MODEL_DIR
18
+ from inference import AIFinder
 
19
 
20
+ app = Flask(__name__)
21
  CORS(app)
22
+ limiter = Limiter(get_remote_address, app=app)
23
 
24
+ finder: AIFinder | None = None
25
+ community_finder: AIFinder | None = None
26
+ using_community = False
27
 
28
+ DEFAULT_TOP_N = 4
29
+ COMMUNITY_DIR = os.path.join(MODEL_DIR, "community")
30
+ CORRECTIONS_FILE = os.path.join(COMMUNITY_DIR, "corrections.joblib")
31
+ corrections: list[dict] = []
 
32
 
33
 
34
  def load_models():
35
+ global finder, community_finder, corrections
36
+ finder = AIFinder(model_dir=MODEL_DIR)
37
+ os.makedirs(COMMUNITY_DIR, exist_ok=True)
38
+ if os.path.exists(CORRECTIONS_FILE):
39
+ corrections = joblib.load(CORRECTIONS_FILE)
40
+ if os.path.exists(os.path.join(COMMUNITY_DIR, "rf_4provider.joblib")):
41
+ try:
42
+ community_finder = AIFinder(model_dir=COMMUNITY_DIR)
43
+ except Exception:
44
+ community_finder = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
 
 
46
 
47
+ def _active_finder():
48
+ return community_finder if using_community and community_finder else finder
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
 
51
  def _strip_think_tags(text):
 
52
  text = re.sub(r"<think(?:ing)?>.*?</think(?:ing)?>", "", text, flags=re.DOTALL)
53
  return text.strip()
54
 
55
 
56
+ @app.route("/")
57
+ def index():
58
+ return render_template("index.html")
59
+
60
+
61
+ @app.route("/api/classify", methods=["POST"])
62
  @app.route("/v1/classify", methods=["POST"])
63
  @limiter.limit("60/minute")
64
  def v1_classify():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  data = request.get_json(silent=True)
66
  if not data or "text" not in data:
67
  return jsonify({"error": "Request body must be JSON with a 'text' field."}), 400
68
 
69
  raw_text = data["text"]
70
  text = _strip_think_tags(raw_text)
71
+ af = _active_finder()
72
+ top_n = min(data.get("top_n", DEFAULT_TOP_N), len(af.le.classes_))
73
 
74
  if not isinstance(top_n, int) or top_n < 1:
75
  top_n = DEFAULT_TOP_N
76
 
77
  if len(text) < 20:
78
+ return jsonify(
79
+ {
80
+ "error": "Text too short (minimum 20 characters after stripping think tags)."
81
+ }
82
+ ), 400
 
 
83
 
84
+ proba = af.predict_proba(text)
85
+ sorted_providers = sorted(proba.items(), key=lambda x: x[1], reverse=True)[:top_n]
86
 
 
87
  top_providers = [
88
+ {"name": name, "confidence": round(float(conf * 100), 2)}
89
+ for name, conf in sorted_providers
 
 
 
90
  ]
91
 
92
  return jsonify(
 
100
 
101
  @app.route("/api/correct", methods=["POST"])
102
  def correct():
103
+ global community_finder
104
+ data = request.get_json(silent=True)
105
+ if not data or "text" not in data or "correct_provider" not in data:
106
+ return jsonify({"error": "Need 'text' and 'correct_provider'."}), 400
 
 
 
 
 
 
 
 
107
 
108
+ provider = data["correct_provider"]
109
+ if provider not in list(finder.le.classes_):
110
+ return jsonify({"error": f"Unknown provider: {provider}"}), 400
111
 
112
+ text = _strip_think_tags(data["text"])
113
+ corrections.append({"text": text, "provider": provider})
 
 
114
 
115
+ texts = [c["text"] for c in corrections]
116
+ providers = [c["provider"] for c in corrections]
117
+ X = finder.pipeline.transform(texts)
118
+ y = finder.le.transform(providers)
119
 
120
+ rf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
121
+ rf.fit(X, y)
 
 
 
 
122
 
123
+ joblib.dump([rf], os.path.join(COMMUNITY_DIR, "rf_4provider.joblib"))
124
+ joblib.dump(finder.pipeline, os.path.join(COMMUNITY_DIR, "pipeline_4provider.joblib"))
125
+ joblib.dump(finder.le, os.path.join(COMMUNITY_DIR, "enc_4provider.joblib"))
126
+ joblib.dump(corrections, CORRECTIONS_FILE)
127
 
128
+ community_finder = AIFinder(model_dir=COMMUNITY_DIR)
129
 
130
+ return jsonify({"status": "ok", "loss": 0.0, "corrections": len(corrections)})
131
 
132
 
133
  @app.route("/api/save", methods=["POST"])
134
  def save_model():
135
+ if community_finder is None:
136
+ return jsonify({"error": "No community model trained yet."}), 400
137
+ filename = "community_rf_4provider.joblib"
138
+ return jsonify({"status": "ok", "filename": filename})
139
 
 
 
140
 
141
+ @app.route("/api/toggle_community", methods=["POST"])
142
+ def toggle_community():
143
+ global using_community
144
+ data = request.get_json(silent=True) or {}
145
+ using_community = bool(data.get("enabled", not using_community))
146
+ return jsonify({"using_community": using_community, "available": community_finder is not None})
147
 
148
 
149
  @app.route("/models/<filename>")
150
  def download_model(filename):
151
+ if filename.startswith("community_"):
152
+ return send_from_directory(COMMUNITY_DIR, filename.replace("community_", "", 1))
153
  return send_from_directory(MODEL_DIR, filename)
154
 
155
 
156
  @app.route("/api/status", methods=["GET"])
157
  def status():
158
+ af = _active_finder()
159
  return jsonify(
160
  {
161
+ "loaded": af is not None,
162
+ "device": "cpu",
163
+ "providers": list(af.le.classes_) if af else [],
164
+ "num_providers": len(af.le.classes_) if af else 0,
165
+ "using_community": using_community,
166
+ "community_available": community_finder is not None,
167
+ "corrections_count": len(corrections),
168
+ }
169
+ )
170
+
171
+
172
+ @app.route("/api/providers", methods=["GET"])
173
+ def providers():
174
+ return jsonify(
175
+ {
176
+ "providers": list(finder.le.classes_) if finder else [],
177
  }
178
  )
179
 
 
181
  if __name__ == "__main__":
182
  print("Loading models...")
183
  load_models()
184
+ print(
185
+ f"Ready on cpu β€” {len(finder.le.classes_)} providers: "
186
+ f"{', '.join(finder.le.classes_)}"
187
+ )
188
  app.run(host="0.0.0.0", port=7860)
config.py CHANGED
@@ -15,91 +15,136 @@ MODEL_DIR = os.path.join(BASE_DIR, "models")
15
  DATASET_REGISTRY = [
16
  # Anthropic
17
  ("TeichAI/claude-4.5-opus-high-reasoning-250x", "Anthropic", "Claude 4.5 Opus", {}),
18
- ("TeichAI/claude-sonnet-4.5-high-reasoning-250x", "Anthropic", "Claude Sonnet 4.5", {}),
19
- ("Roman1111111/claude-opus-4.6-10000x", "Anthropic", "Claude Opus 4.6", {"max_samples": 1500}),
20
-
 
 
 
 
 
 
 
 
 
21
  # OpenAI
22
  ("TeichAI/gpt-5.2-high-reasoning-250x", "OpenAI", "GPT-5.2", {}),
23
  ("TeichAI/gpt-5.1-high-reasoning-1000x", "OpenAI", "GPT-5.1", {}),
24
  ("TeichAI/gpt-5.1-codex-max-1000x", "OpenAI", "GPT-5.1 Codex Max", {}),
25
  ("TeichAI/gpt-5-codex-250x", "OpenAI", "GPT-5 Codex", {}),
26
  ("TeichAI/gpt-5-codex-1000x", "OpenAI", "GPT-5 Codex", {}),
27
-
28
  # Google
29
  ("TeichAI/gemini-3-pro-preview-high-reasoning-1000x", "Google", "Gemini 3 Pro", {}),
30
  ("TeichAI/gemini-3-pro-preview-high-reasoning-250x", "Google", "Gemini 3 Pro", {}),
31
- ("TeichAI/gemini-2.5-flash-11000x", "Google", "Gemini 2.5 Flash", {"max_samples": 1500}),
 
 
 
 
 
32
  ("TeichAI/Gemini-3-Flash-Preview-VIBE", "Google", "Gemini 3 Flash", {}),
33
  ("TeichAI/gemini-3-flash-preview-1000x", "Google", "Gemini 3 Flash", {}),
34
  ("TeichAI/gemini-3-flash-preview-complex-1000x", "Google", "Gemini 3 Flash", {}),
35
-
36
  # xAI
37
  ("TeichAI/brainstorm-v3.1-grok-4-fast-200x", "xAI", "Grok 4 Fast", {}),
38
- ("TeichAI/sherlock-thinking-alpha-11000x", "xAI", "Grok 4.1 Fast", {"max_samples": 1500}),
 
 
 
 
 
39
  ("TeichAI/sherlock-dash-alpha-1000x", "xAI", "Grok 4.1 Fast", {}),
40
  ("TeichAI/sherlock-think-alpha-1000x", "xAI", "Grok 4.1 Fast", {}),
41
  ("TeichAI/grok-code-fast-1-1000x", "xAI", "Grok Code Fast 1", {}),
42
-
43
  # MoonshotAI
44
  ("TeichAI/kimi-k2-thinking-250x", "MoonshotAI", "Kimi K2", {}),
45
  ("TeichAI/kimi-k2-thinking-1000x", "MoonshotAI", "Kimi K2", {}),
46
-
47
  # Mistral
48
  ("TeichAI/mistral-small-creative-500x", "Mistral", "Mistral Small", {}),
49
-
50
  # MiniMax
51
- ("TeichAI/MiniMax-M2.1-Code-SFT", "MiniMax", "MiniMax M2.1", {}),
52
  ("TeichAI/convo-v1", "MiniMax", "MiniMax M2.1", {}),
53
-
54
  # StepFun
55
- ("TeichAI/Step-3.5-Flash-2600x", "StepFun", "Step 3.5 Flash", {"max_samples": 1500}),
56
-
 
 
 
 
57
  # Zhipu
58
  ("TeichAI/Pony-Alpha-15k", "Zhipu", "GLM-5", {"max_samples": 1500}),
59
-
60
  # DeepSeek (TeichAI)
61
  ("TeichAI/deepseek-v3.2-speciale-1000x", "DeepSeek", "DeepSeek V3.2 Speciale", {}),
62
- ("TeichAI/deepseek-v3.2-speciale-openr1-math-3k", "DeepSeek", "DeepSeek V3.2 Speciale", {"max_samples": 1500}),
 
 
 
 
 
63
  ]
64
 
65
  # DeepSeek (a-m-team) β€” different format, handled separately
66
  DEEPSEEK_AM_DATASETS = [
67
- ("a-m-team/AM-DeepSeek-R1-Distilled-1.4M", "DeepSeek", "DeepSeek R1", {"name": "am_0.9M_sample_1k", "max_samples": 1000}),
 
 
 
 
 
68
  ]
69
 
 
 
 
70
  # --- All providers and models ---
71
  PROVIDERS = [
72
- "Anthropic", "OpenAI", "Google", "xAI", "MoonshotAI",
73
- "Mistral", "MiniMax", "StepFun", "Zhipu", "DeepSeek"
 
 
 
 
 
 
 
 
74
  ]
75
 
76
  # --- Feature parameters ---
77
  TFIDF_WORD_PARAMS = {
78
  "analyzer": "word",
79
  "ngram_range": (1, 2),
80
- "max_features": 20000,
81
  "sublinear_tf": True,
82
  "min_df": 3,
 
83
  }
84
 
85
  TFIDF_CHAR_PARAMS = {
86
  "analyzer": "char_wb",
87
- "ngram_range": (3, 5),
88
- "max_features": 20000,
89
  "sublinear_tf": True,
90
  "min_df": 3,
 
 
91
  }
92
 
93
- # --- Train/test split ---
94
- TEST_SIZE = 0.2
 
 
 
 
95
  RANDOM_STATE = 42
96
 
97
  # --- Neural Network ---
98
- HIDDEN_DIM = 1024
99
- EMBED_DIM = 256
100
- DROPOUT = 0.3
101
- BATCH_SIZE = 2048
102
- EPOCHS = 50
103
- EARLY_STOP_PATIENCE = 8
104
- LEARNING_RATE = 1e-3
105
- WEIGHT_DECAY = 1e-4
 
 
15
  DATASET_REGISTRY = [
16
  # Anthropic
17
  ("TeichAI/claude-4.5-opus-high-reasoning-250x", "Anthropic", "Claude 4.5 Opus", {}),
18
+ (
19
+ "TeichAI/claude-sonnet-4.5-high-reasoning-250x",
20
+ "Anthropic",
21
+ "Claude Sonnet 4.5",
22
+ {},
23
+ ),
24
+ (
25
+ "Roman1111111/claude-opus-4.6-10000x",
26
+ "Anthropic",
27
+ "Claude Opus 4.6",
28
+ {"max_samples": 1500},
29
+ ),
30
  # OpenAI
31
  ("TeichAI/gpt-5.2-high-reasoning-250x", "OpenAI", "GPT-5.2", {}),
32
  ("TeichAI/gpt-5.1-high-reasoning-1000x", "OpenAI", "GPT-5.1", {}),
33
  ("TeichAI/gpt-5.1-codex-max-1000x", "OpenAI", "GPT-5.1 Codex Max", {}),
34
  ("TeichAI/gpt-5-codex-250x", "OpenAI", "GPT-5 Codex", {}),
35
  ("TeichAI/gpt-5-codex-1000x", "OpenAI", "GPT-5 Codex", {}),
 
36
  # Google
37
  ("TeichAI/gemini-3-pro-preview-high-reasoning-1000x", "Google", "Gemini 3 Pro", {}),
38
  ("TeichAI/gemini-3-pro-preview-high-reasoning-250x", "Google", "Gemini 3 Pro", {}),
39
+ (
40
+ "TeichAI/gemini-2.5-flash-11000x",
41
+ "Google",
42
+ "Gemini 2.5 Flash",
43
+ {"max_samples": 1500},
44
+ ),
45
  ("TeichAI/Gemini-3-Flash-Preview-VIBE", "Google", "Gemini 3 Flash", {}),
46
  ("TeichAI/gemini-3-flash-preview-1000x", "Google", "Gemini 3 Flash", {}),
47
  ("TeichAI/gemini-3-flash-preview-complex-1000x", "Google", "Gemini 3 Flash", {}),
 
48
  # xAI
49
  ("TeichAI/brainstorm-v3.1-grok-4-fast-200x", "xAI", "Grok 4 Fast", {}),
50
+ (
51
+ "TeichAI/sherlock-thinking-alpha-11000x",
52
+ "xAI",
53
+ "Grok 4.1 Fast",
54
+ {"max_samples": 1500},
55
+ ),
56
  ("TeichAI/sherlock-dash-alpha-1000x", "xAI", "Grok 4.1 Fast", {}),
57
  ("TeichAI/sherlock-think-alpha-1000x", "xAI", "Grok 4.1 Fast", {}),
58
  ("TeichAI/grok-code-fast-1-1000x", "xAI", "Grok Code Fast 1", {}),
 
59
  # MoonshotAI
60
  ("TeichAI/kimi-k2-thinking-250x", "MoonshotAI", "Kimi K2", {}),
61
  ("TeichAI/kimi-k2-thinking-1000x", "MoonshotAI", "Kimi K2", {}),
 
62
  # Mistral
63
  ("TeichAI/mistral-small-creative-500x", "Mistral", "Mistral Small", {}),
 
64
  # MiniMax
65
+ ("TeichAI/MiniMax-M2.1-Code-SFT", "MiniMax", "MiniMax M2.1", {"max_samples": 1500}),
66
  ("TeichAI/convo-v1", "MiniMax", "MiniMax M2.1", {}),
 
67
  # StepFun
68
+ (
69
+ "TeichAI/Step-3.5-Flash-2600x",
70
+ "StepFun",
71
+ "Step 3.5 Flash",
72
+ {"max_samples": 1500},
73
+ ),
74
  # Zhipu
75
  ("TeichAI/Pony-Alpha-15k", "Zhipu", "GLM-5", {"max_samples": 1500}),
 
76
  # DeepSeek (TeichAI)
77
  ("TeichAI/deepseek-v3.2-speciale-1000x", "DeepSeek", "DeepSeek V3.2 Speciale", {}),
78
+ (
79
+ "TeichAI/deepseek-v3.2-speciale-openr1-math-3k",
80
+ "DeepSeek",
81
+ "DeepSeek V3.2 Speciale",
82
+ {"max_samples": 1500},
83
+ ),
84
  ]
85
 
86
  # DeepSeek (a-m-team) β€” different format, handled separately
87
  DEEPSEEK_AM_DATASETS = [
88
+ (
89
+ "a-m-team/AM-DeepSeek-R1-Distilled-1.4M",
90
+ "DeepSeek",
91
+ "DeepSeek R1",
92
+ {"name": "am_0.9M", "max_samples": 1000},
93
+ ),
94
  ]
95
 
96
+ # Conversational datasets disabled
97
+ CONVERSATIONAL_DATASETS = []
98
+
99
  # --- All providers and models ---
100
  PROVIDERS = [
101
+ "Anthropic",
102
+ "OpenAI",
103
+ "Google",
104
+ "xAI",
105
+ "MoonshotAI",
106
+ "Mistral",
107
+ "MiniMax",
108
+ "StepFun",
109
+ "Zhipu",
110
+ "DeepSeek",
111
  ]
112
 
113
  # --- Feature parameters ---
114
  TFIDF_WORD_PARAMS = {
115
  "analyzer": "word",
116
  "ngram_range": (1, 2),
117
+ "max_features": 20,
118
  "sublinear_tf": True,
119
  "min_df": 3,
120
+ "max_df": 0.7,
121
  }
122
 
123
  TFIDF_CHAR_PARAMS = {
124
  "analyzer": "char_wb",
125
+ "ngram_range": (2, 4),
126
+ "max_features": 20,
127
  "sublinear_tf": True,
128
  "min_df": 3,
129
+ "max_df": 0.7,
130
+ "smooth_idf": True,
131
  }
132
 
133
+ # Equal samples per provider
134
+ MAX_SAMPLES_PER_PROVIDER = 1000
135
+
136
+ # --- Train/val/test split ---
137
+ TEST_SIZE = 0.15
138
+ VAL_SIZE = 0.10
139
  RANDOM_STATE = 42
140
 
141
  # --- Neural Network ---
142
+ HIDDEN_DIM = 256
143
+ EMBED_DIM = 128
144
+ DROPOUT = 0.7
145
+ BATCH_SIZE = 128
146
+ EPOCHS = 80
147
+ EARLY_STOP_PATIENCE = 25
148
+ LEARNING_RATE = 3e-5
149
+ WEIGHT_DECAY = 8e-2
150
+ LABEL_SMOOTHING = 0.3
inference.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AIFinder Inference Module
3
+ Load the trained model and predict AI provider
4
+ """
5
+
6
+ import joblib
7
+ import numpy as np
8
+
9
+ from config import MODEL_DIR
10
+
11
+
12
+ class AIFinder:
13
+ def __init__(self, model_dir=MODEL_DIR):
14
+ self.models = joblib.load(f"{model_dir}/rf_4provider.joblib")
15
+ self.pipeline = joblib.load(f"{model_dir}/pipeline_4provider.joblib")
16
+ self.le = joblib.load(f"{model_dir}/enc_4provider.joblib")
17
+
18
+ def predict(self, text):
19
+ """Predict the provider for a given text"""
20
+ X = self.pipeline.transform([text])
21
+ proba = np.mean([m.predict_proba(X) for m in self.models], axis=0)
22
+ pred_idx = np.argmax(proba[0])
23
+ return self.le.classes_[pred_idx]
24
+
25
+ def predict_proba(self, text):
26
+ """Get prediction probabilities"""
27
+ X = self.pipeline.transform([text])
28
+ proba = np.mean([m.predict_proba(X) for m in self.models], axis=0)
29
+ return dict(zip(self.le.classes_, proba[0]))
30
+
31
+ def predict_with_confidence(self, text):
32
+ """Predict with confidence score"""
33
+ proba = self.predict_proba(text)
34
+ provider = max(proba, key=proba.get)
35
+ confidence = proba[provider]
36
+ return provider, confidence
37
+
38
+
39
+ if __name__ == "__main__":
40
+ finder = AIFinder()
41
+
42
+ test_texts = [
43
+ "AI is like a really smart robot helper.",
44
+ "Yes, coding is one of my stronger skills!",
45
+ "A lot, depending on what you need.",
46
+ ]
47
+
48
+ for text in test_texts:
49
+ provider, conf = finder.predict_with_confidence(text)
50
+ print(f"Text: {text[:50]}...")
51
+ print(f"Provider: {provider} (confidence: {conf:.2f})")
52
+ print()
models/1773449581 (1).png ADDED

Git LFS Details

  • SHA256: c4040e312991db4fff1cb3aa18184f4b7477878e5642666748b13b98d60815db
  • Pointer size: 132 Bytes
  • Size of remote file: 2.08 MB
models/enc_4provider.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9125a24e56ba5808ed41a62f5d321134e5cc6b23b862679ea15f09ea6dc64985
3
+ size 471
models/pipeline_4provider.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:86b1810b0c76194f98e785c24cecb911948f21f3410cf57d43e74def8f967b20
3
+ size 5015
models/rf_4provider.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c24c29e35c11b789f9b1eb5dc562f68d0621a963b598c0b391aaad0b02162a73
3
+ size 44121754
templates/index.html ADDED
@@ -0,0 +1,1373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AIFinder - Identify AI Responses</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap');
9
+
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ :root {
17
+ --bg-primary: #0d0d0d;
18
+ --bg-secondary: #171717;
19
+ --bg-tertiary: #1f1f1f;
20
+ --bg-elevated: #262626;
21
+ --text-primary: #f5f5f5;
22
+ --text-secondary: #a3a3a3;
23
+ --text-muted: #737373;
24
+ --accent: #e85d04;
25
+ --accent-hover: #f48c06;
26
+ --accent-muted: #9c4300;
27
+ --success: #22c55e;
28
+ --success-muted: #166534;
29
+ --border: #333333;
30
+ --border-light: #404040;
31
+ }
32
+
33
+ body {
34
+ font-family: 'Outfit', -apple-system, sans-serif;
35
+ background: var(--bg-primary);
36
+ color: var(--text-primary);
37
+ min-height: 100vh;
38
+ line-height: 1.6;
39
+ }
40
+
41
+ .container {
42
+ max-width: 900px;
43
+ margin: 0 auto;
44
+ padding: 2rem 1.5rem;
45
+ }
46
+
47
+ header {
48
+ text-align: center;
49
+ margin-bottom: 3rem;
50
+ padding-top: 1rem;
51
+ }
52
+
53
+ .logo {
54
+ font-size: 2.5rem;
55
+ font-weight: 700;
56
+ letter-spacing: -0.05em;
57
+ margin-bottom: 0.5rem;
58
+ }
59
+
60
+ .logo span {
61
+ color: var(--accent);
62
+ }
63
+
64
+ .tagline {
65
+ color: var(--text-secondary);
66
+ font-size: 1rem;
67
+ font-weight: 300;
68
+ }
69
+
70
+ .card {
71
+ background: var(--bg-secondary);
72
+ border: 1px solid var(--border);
73
+ border-radius: 12px;
74
+ padding: 1.5rem;
75
+ margin-bottom: 1.5rem;
76
+ transition: border-color 0.2s ease;
77
+ }
78
+
79
+ .card:focus-within {
80
+ border-color: var(--border-light);
81
+ }
82
+
83
+ .card-label {
84
+ font-size: 0.75rem;
85
+ text-transform: uppercase;
86
+ letter-spacing: 0.1em;
87
+ color: var(--text-muted);
88
+ margin-bottom: 0.75rem;
89
+ font-weight: 500;
90
+ }
91
+
92
+ textarea {
93
+ width: 100%;
94
+ background: var(--bg-tertiary);
95
+ border: 1px solid var(--border);
96
+ border-radius: 8px;
97
+ padding: 1rem;
98
+ color: var(--text-primary);
99
+ font-family: 'JetBrains Mono', monospace;
100
+ font-size: 0.875rem;
101
+ resize: vertical;
102
+ min-height: 180px;
103
+ transition: border-color 0.2s ease;
104
+ }
105
+
106
+ textarea:focus {
107
+ outline: none;
108
+ border-color: var(--accent-muted);
109
+ }
110
+
111
+ textarea::placeholder {
112
+ color: var(--text-muted);
113
+ }
114
+
115
+ .btn {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ gap: 0.5rem;
120
+ padding: 0.75rem 1.5rem;
121
+ border-radius: 8px;
122
+ font-family: 'Outfit', sans-serif;
123
+ font-size: 0.9rem;
124
+ font-weight: 500;
125
+ cursor: pointer;
126
+ transition: all 0.2s ease;
127
+ border: none;
128
+ }
129
+
130
+ .btn-primary {
131
+ background: var(--accent);
132
+ color: white;
133
+ }
134
+
135
+ .btn-primary:hover:not(:disabled) {
136
+ background: var(--accent-hover);
137
+ }
138
+
139
+ .btn-primary:disabled {
140
+ opacity: 0.5;
141
+ cursor: not-allowed;
142
+ }
143
+
144
+ .btn-secondary {
145
+ background: var(--bg-tertiary);
146
+ color: var(--text-primary);
147
+ border: 1px solid var(--border);
148
+ }
149
+
150
+ .btn-secondary:hover:not(:disabled) {
151
+ background: var(--bg-elevated);
152
+ border-color: var(--border-light);
153
+ }
154
+
155
+ .btn-group {
156
+ display: flex;
157
+ gap: 0.75rem;
158
+ flex-wrap: wrap;
159
+ }
160
+
161
+ .results {
162
+ display: none;
163
+ }
164
+
165
+ .results.visible {
166
+ display: block;
167
+ animation: fadeIn 0.3s ease;
168
+ }
169
+
170
+ @keyframes fadeIn {
171
+ from { opacity: 0; transform: translateY(10px); }
172
+ to { opacity: 1; transform: translateY(0); }
173
+ }
174
+
175
+ .result-main {
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: space-between;
179
+ padding: 1.25rem;
180
+ background: var(--bg-tertiary);
181
+ border-radius: 8px;
182
+ margin-bottom: 1rem;
183
+ }
184
+
185
+ .result-provider {
186
+ font-size: 1.5rem;
187
+ font-weight: 600;
188
+ }
189
+
190
+ .result-confidence {
191
+ font-size: 1.25rem;
192
+ font-weight: 500;
193
+ color: var(--accent);
194
+ }
195
+
196
+ .result-bar {
197
+ height: 8px;
198
+ background: var(--bg-elevated);
199
+ border-radius: 4px;
200
+ margin-bottom: 1rem;
201
+ overflow: hidden;
202
+ }
203
+
204
+ .result-bar-fill {
205
+ height: 100%;
206
+ background: var(--accent);
207
+ border-radius: 4px;
208
+ transition: width 0.5s ease;
209
+ }
210
+
211
+ .result-list {
212
+ list-style: none;
213
+ }
214
+
215
+ .result-item {
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: space-between;
219
+ padding: 0.75rem 0;
220
+ border-bottom: 1px solid var(--border);
221
+ }
222
+
223
+ .result-item:last-child {
224
+ border-bottom: none;
225
+ }
226
+
227
+ .result-name {
228
+ font-weight: 500;
229
+ }
230
+
231
+ .result-percent {
232
+ font-family: 'JetBrains Mono', monospace;
233
+ color: var(--text-secondary);
234
+ font-size: 0.875rem;
235
+ }
236
+
237
+ .correction {
238
+ display: none;
239
+ margin-top: 1.5rem;
240
+ padding-top: 1.5rem;
241
+ border-top: 1px solid var(--border);
242
+ }
243
+
244
+ .correction.visible {
245
+ display: block;
246
+ animation: fadeIn 0.3s ease;
247
+ }
248
+
249
+ .correction-title {
250
+ font-size: 0.875rem;
251
+ font-weight: 500;
252
+ margin-bottom: 0.75rem;
253
+ color: var(--text-secondary);
254
+ }
255
+
256
+ select {
257
+ width: 100%;
258
+ padding: 0.75rem 1rem;
259
+ background: var(--bg-tertiary);
260
+ border: 1px solid var(--border);
261
+ border-radius: 8px;
262
+ color: var(--text-primary);
263
+ font-family: 'Outfit', sans-serif;
264
+ font-size: 0.9rem;
265
+ margin-bottom: 0.75rem;
266
+ cursor: pointer;
267
+ }
268
+
269
+ select:focus {
270
+ outline: none;
271
+ border-color: var(--accent-muted);
272
+ }
273
+
274
+ .stats {
275
+ display: flex;
276
+ gap: 1.5rem;
277
+ margin-bottom: 1.5rem;
278
+ flex-wrap: wrap;
279
+ }
280
+
281
+ .stat {
282
+ background: var(--bg-secondary);
283
+ border: 1px solid var(--border);
284
+ border-radius: 8px;
285
+ padding: 1rem 1.25rem;
286
+ flex: 1;
287
+ min-width: 120px;
288
+ }
289
+
290
+ .stat-value {
291
+ font-size: 1.5rem;
292
+ font-weight: 600;
293
+ color: var(--accent);
294
+ }
295
+
296
+ .stat-label {
297
+ font-size: 0.75rem;
298
+ color: var(--text-muted);
299
+ text-transform: uppercase;
300
+ letter-spacing: 0.05em;
301
+ }
302
+
303
+ .actions {
304
+ display: flex;
305
+ gap: 0.75rem;
306
+ margin-top: 1rem;
307
+ }
308
+
309
+ .toast {
310
+ position: fixed;
311
+ bottom: 2rem;
312
+ right: 2rem;
313
+ background: var(--bg-elevated);
314
+ border: 1px solid var(--border);
315
+ border-radius: 8px;
316
+ padding: 1rem 1.5rem;
317
+ color: var(--text-primary);
318
+ font-size: 0.9rem;
319
+ opacity: 0;
320
+ transform: translateY(20px);
321
+ transition: all 0.3s ease;
322
+ z-index: 1000;
323
+ }
324
+
325
+ .toast.visible {
326
+ opacity: 1;
327
+ transform: translateY(0);
328
+ }
329
+
330
+ .toast.success {
331
+ border-color: var(--success-muted);
332
+ }
333
+
334
+ .footer {
335
+ text-align: center;
336
+ margin-top: 3rem;
337
+ padding: 1.5rem;
338
+ color: var(--text-muted);
339
+ font-size: 0.8rem;
340
+ }
341
+
342
+ .footer a {
343
+ color: var(--text-secondary);
344
+ text-decoration: none;
345
+ }
346
+
347
+ .footer a:hover {
348
+ color: var(--accent);
349
+ }
350
+
351
+ .loading {
352
+ display: inline-block;
353
+ width: 16px;
354
+ height: 16px;
355
+ border: 2px solid var(--text-muted);
356
+ border-top-color: var(--accent);
357
+ border-radius: 50%;
358
+ animation: spin 0.8s linear infinite;
359
+ }
360
+
361
+ @keyframes spin {
362
+ to { transform: rotate(360deg); }
363
+ }
364
+
365
+ .status-indicator {
366
+ display: inline-flex;
367
+ align-items: center;
368
+ gap: 0.5rem;
369
+ font-size: 0.8rem;
370
+ color: var(--text-muted);
371
+ margin-bottom: 1rem;
372
+ }
373
+
374
+ .status-dot {
375
+ width: 8px;
376
+ height: 8px;
377
+ border-radius: 50%;
378
+ background: var(--success);
379
+ }
380
+
381
+ .status-dot.loading {
382
+ background: var(--accent);
383
+ animation: pulse 1s ease infinite;
384
+ }
385
+
386
+ @keyframes pulse {
387
+ 0%, 100% { opacity: 1; }
388
+ 50% { opacity: 0.5; }
389
+ }
390
+
391
+ .empty-state {
392
+ text-align: center;
393
+ padding: 3rem 1rem;
394
+ color: var(--text-muted);
395
+ }
396
+
397
+ .empty-state-icon {
398
+ font-size: 3rem;
399
+ margin-bottom: 1rem;
400
+ opacity: 0.5;
401
+ }
402
+
403
+ /* ── Tabs ── */
404
+ .tabs {
405
+ display: flex;
406
+ gap: 0;
407
+ margin-bottom: 2rem;
408
+ border-bottom: 1px solid var(--border);
409
+ }
410
+
411
+ .tab {
412
+ padding: 0.75rem 1.5rem;
413
+ font-family: 'Outfit', sans-serif;
414
+ font-size: 0.9rem;
415
+ font-weight: 500;
416
+ color: var(--text-muted);
417
+ background: none;
418
+ border: none;
419
+ border-bottom: 2px solid transparent;
420
+ cursor: pointer;
421
+ transition: all 0.2s ease;
422
+ }
423
+
424
+ .tab:hover {
425
+ color: var(--text-secondary);
426
+ }
427
+
428
+ .tab.active {
429
+ color: var(--accent);
430
+ border-bottom-color: var(--accent);
431
+ }
432
+
433
+ .tab-content {
434
+ display: none;
435
+ }
436
+
437
+ .tab-content.active {
438
+ display: block;
439
+ animation: fadeIn 0.3s ease;
440
+ }
441
+
442
+ /* ── API Docs ── */
443
+ .docs-section {
444
+ margin-bottom: 2rem;
445
+ }
446
+
447
+ .docs-section h2 {
448
+ font-size: 1.25rem;
449
+ font-weight: 600;
450
+ margin-bottom: 0.75rem;
451
+ color: var(--text-primary);
452
+ }
453
+
454
+ .docs-section h3 {
455
+ font-size: 1rem;
456
+ font-weight: 500;
457
+ margin-top: 1.25rem;
458
+ margin-bottom: 0.5rem;
459
+ color: var(--text-secondary);
460
+ }
461
+
462
+ .docs-section p {
463
+ color: var(--text-secondary);
464
+ font-size: 0.9rem;
465
+ margin-bottom: 0.75rem;
466
+ line-height: 1.7;
467
+ }
468
+
469
+ .docs-endpoint {
470
+ display: inline-flex;
471
+ align-items: center;
472
+ gap: 0.5rem;
473
+ background: var(--bg-tertiary);
474
+ border: 1px solid var(--border);
475
+ border-radius: 6px;
476
+ padding: 0.5rem 1rem;
477
+ margin-bottom: 1rem;
478
+ font-family: 'JetBrains Mono', monospace;
479
+ font-size: 0.85rem;
480
+ }
481
+
482
+ .docs-method {
483
+ color: var(--success);
484
+ font-weight: 600;
485
+ }
486
+
487
+ .docs-path {
488
+ color: var(--text-primary);
489
+ }
490
+
491
+ .docs-badge {
492
+ display: inline-block;
493
+ font-size: 0.7rem;
494
+ font-weight: 600;
495
+ text-transform: uppercase;
496
+ letter-spacing: 0.05em;
497
+ padding: 0.2rem 0.6rem;
498
+ border-radius: 4px;
499
+ margin-left: 0.5rem;
500
+ }
501
+
502
+ .docs-badge.free {
503
+ background: var(--success-muted);
504
+ color: var(--success);
505
+ }
506
+
507
+ .docs-badge.limit {
508
+ background: var(--accent-muted);
509
+ color: var(--accent-hover);
510
+ }
511
+
512
+ .docs-code-block {
513
+ position: relative;
514
+ background: var(--bg-tertiary);
515
+ border: 1px solid var(--border);
516
+ border-radius: 8px;
517
+ margin-bottom: 1rem;
518
+ overflow: hidden;
519
+ }
520
+
521
+ .docs-code-header {
522
+ display: flex;
523
+ align-items: center;
524
+ justify-content: space-between;
525
+ padding: 0.5rem 1rem;
526
+ background: var(--bg-elevated);
527
+ border-bottom: 1px solid var(--border);
528
+ font-size: 0.75rem;
529
+ color: var(--text-muted);
530
+ text-transform: uppercase;
531
+ letter-spacing: 0.05em;
532
+ }
533
+
534
+ .docs-copy-btn {
535
+ background: none;
536
+ border: 1px solid var(--border);
537
+ border-radius: 4px;
538
+ color: var(--text-muted);
539
+ font-size: 0.7rem;
540
+ padding: 0.2rem 0.5rem;
541
+ cursor: pointer;
542
+ font-family: 'Outfit', sans-serif;
543
+ transition: all 0.2s ease;
544
+ }
545
+
546
+ .docs-copy-btn:hover {
547
+ color: var(--text-primary);
548
+ border-color: var(--border-light);
549
+ }
550
+
551
+ .docs-code-block pre {
552
+ padding: 1rem;
553
+ overflow-x: auto;
554
+ font-family: 'JetBrains Mono', monospace;
555
+ font-size: 0.8rem;
556
+ line-height: 1.6;
557
+ color: var(--text-primary);
558
+ margin: 0;
559
+ }
560
+
561
+ .docs-table {
562
+ width: 100%;
563
+ border-collapse: collapse;
564
+ font-size: 0.85rem;
565
+ margin-bottom: 1rem;
566
+ }
567
+
568
+ .docs-table th {
569
+ text-align: left;
570
+ padding: 0.6rem 0.75rem;
571
+ background: var(--bg-elevated);
572
+ color: var(--text-secondary);
573
+ font-weight: 500;
574
+ border-bottom: 1px solid var(--border);
575
+ font-size: 0.75rem;
576
+ text-transform: uppercase;
577
+ letter-spacing: 0.05em;
578
+ }
579
+
580
+ .docs-table td {
581
+ padding: 0.6rem 0.75rem;
582
+ border-bottom: 1px solid var(--border);
583
+ color: var(--text-secondary);
584
+ }
585
+
586
+ .docs-table tr:last-child td {
587
+ border-bottom: none;
588
+ }
589
+
590
+ .docs-table code {
591
+ font-family: 'JetBrains Mono', monospace;
592
+ font-size: 0.8rem;
593
+ background: var(--bg-tertiary);
594
+ padding: 0.15rem 0.4rem;
595
+ border-radius: 3px;
596
+ color: var(--accent-hover);
597
+ }
598
+
599
+ .docs-warning {
600
+ background: rgba(232, 93, 4, 0.08);
601
+ border: 1px solid var(--accent-muted);
602
+ border-radius: 8px;
603
+ padding: 1rem 1.25rem;
604
+ margin-bottom: 1rem;
605
+ font-size: 0.85rem;
606
+ color: var(--text-secondary);
607
+ line-height: 1.7;
608
+ }
609
+
610
+ .docs-warning strong {
611
+ color: var(--accent-hover);
612
+ }
613
+
614
+ .docs-inline-code {
615
+ font-family: 'JetBrains Mono', monospace;
616
+ font-size: 0.8rem;
617
+ background: var(--bg-tertiary);
618
+ padding: 0.15rem 0.4rem;
619
+ border-radius: 3px;
620
+ color: var(--accent-hover);
621
+ }
622
+
623
+ .docs-try-it {
624
+ background: var(--bg-tertiary);
625
+ border: 1px solid var(--border);
626
+ border-radius: 8px;
627
+ padding: 1.25rem;
628
+ margin-top: 1rem;
629
+ }
630
+
631
+ .docs-try-it textarea {
632
+ min-height: 100px;
633
+ margin-bottom: 0.75rem;
634
+ }
635
+
636
+ .docs-try-output {
637
+ background: var(--bg-primary);
638
+ border: 1px solid var(--border);
639
+ border-radius: 6px;
640
+ padding: 1rem;
641
+ font-family: 'JetBrains Mono', monospace;
642
+ font-size: 0.8rem;
643
+ color: var(--text-secondary);
644
+ white-space: pre-wrap;
645
+ word-break: break-word;
646
+ max-height: 300px;
647
+ overflow-y: auto;
648
+ display: none;
649
+ }
650
+
651
+ .docs-try-output.visible {
652
+ display: block;
653
+ animation: fadeIn 0.3s ease;
654
+ }
655
+
656
+ @media (max-width: 600px) {
657
+ .container {
658
+ padding: 1rem;
659
+ }
660
+
661
+ .logo {
662
+ font-size: 2rem;
663
+ }
664
+
665
+ .btn-group {
666
+ flex-direction: column;
667
+ }
668
+
669
+ .btn {
670
+ width: 100%;
671
+ }
672
+
673
+ .result-main {
674
+ flex-direction: column;
675
+ gap: 0.5rem;
676
+ text-align: center;
677
+ }
678
+ }
679
+ </style>
680
+ </head>
681
+ <body>
682
+ <div class="container">
683
+ <header>
684
+ <div class="logo">AI<span>Finder</span></div>
685
+ <p class="tagline">Identify which AI provider generated a response</p>
686
+ </header>
687
+
688
+ <div class="tabs">
689
+ <button class="tab active" data-tab="classify">Classify</button>
690
+ <button class="tab" data-tab="docs">API Docs</button>
691
+ </div>
692
+
693
+ <!-- ═══ Classify Tab ═══ -->
694
+ <div class="tab-content active" id="tab-classify">
695
+ <div class="status-indicator">
696
+ <span class="status-dot" id="statusDot"></span>
697
+ <span id="statusText">Connecting to API...</span>
698
+ </div>
699
+
700
+ <div class="card">
701
+ <div class="card-label">Paste AI Response</div>
702
+ <textarea id="inputText" placeholder="Paste an AI response here to identify which provider generated it..."></textarea>
703
+ </div>
704
+
705
+ <div class="btn-group">
706
+ <button class="btn btn-primary" id="classifyBtn" disabled>
707
+ <span id="classifyBtnText">Classify</span>
708
+ </button>
709
+ <button class="btn btn-secondary" id="clearBtn">Clear</button>
710
+ </div>
711
+
712
+ <div class="results" id="results">
713
+ <div class="card">
714
+ <div class="card-label">Result</div>
715
+ <div class="result-main">
716
+ <span class="result-provider" id="resultProvider">-</span>
717
+ <span class="result-confidence" id="resultConfidence">-</span>
718
+ </div>
719
+ <div class="result-bar">
720
+ <div class="result-bar-fill" id="resultBar" style="width: 0%"></div>
721
+ </div>
722
+ <ul class="result-list" id="resultList"></ul>
723
+ </div>
724
+
725
+ <div class="correction" id="correction">
726
+ <div class="correction-title">Wrong? Correct the provider to train the model:</div>
727
+ <select id="providerSelect"></select>
728
+ <button class="btn btn-primary" id="trainBtn">Train & Save</button>
729
+ </div>
730
+ </div>
731
+
732
+ <div class="stats" id="stats" style="display: none;">
733
+ <div class="stat">
734
+ <div class="stat-value" id="correctionsCount">0</div>
735
+ <div class="stat-label">Corrections</div>
736
+ </div>
737
+ <div class="stat">
738
+ <div class="stat-value" id="sessionCount">0</div>
739
+ <div class="stat-label">Session</div>
740
+ </div>
741
+ </div>
742
+
743
+ <div class="actions" id="actions" style="display: none;">
744
+ <button class="btn btn-secondary" id="exportBtn">Export Trained Model</button>
745
+ <button class="btn btn-secondary" id="communityBtn" style="display:none;">Use Community Model</button>
746
+ <button class="btn btn-secondary" id="resetBtn">Reset Training</button>
747
+ </div>
748
+
749
+ <div id="communityWarning" style="display:none; margin-top:1rem; background:rgba(232,93,4,0.12); border:1px solid var(--accent-muted); border-radius:8px; padding:1rem 1.25rem; font-size:0.85rem; color:var(--text-secondary); line-height:1.7;">
750
+ ⚠️ <strong style="color:var(--accent-hover);">Community Model Active</strong> β€” This is a community-trained version. It could be <strong style="color:var(--accent-hover);">VERY wrong</strong>. Results may be unreliable. Use at your own risk.
751
+ </div>
752
+ </div>
753
+
754
+ <!-- ═══ API Docs Tab ═══ -->
755
+ <div class="tab-content" id="tab-docs">
756
+
757
+ <div class="docs-section">
758
+ <h2>Public Classification API</h2>
759
+ <p>
760
+ AIFinder exposes a free, public endpoint for programmatic classification.
761
+ No API key required.
762
+ </p>
763
+ <div>
764
+ <div class="docs-endpoint">
765
+ <span class="docs-method">POST</span>
766
+ <span class="docs-path">/v1/classify</span>
767
+ </div>
768
+ <span class="docs-badge free">No API Key</span>
769
+ <span class="docs-badge limit">60 req/min</span>
770
+ </div>
771
+ </div>
772
+
773
+ <!-- ── Request ── -->
774
+ <div class="docs-section">
775
+ <h2>Request</h2>
776
+ <p>Send a JSON body with <span class="docs-inline-code">Content-Type: application/json</span>.</p>
777
+
778
+ <table class="docs-table">
779
+ <thead>
780
+ <tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr>
781
+ </thead>
782
+ <tbody>
783
+ <tr>
784
+ <td><code>text</code></td>
785
+ <td>string</td>
786
+ <td>Yes</td>
787
+ <td>The AI-generated text to classify (min 20 chars)</td>
788
+ </tr>
789
+ <tr>
790
+ <td><code>top_n</code></td>
791
+ <td>integer</td>
792
+ <td>No</td>
793
+ <td>Number of results to return (default: <strong>5</strong>)</td>
794
+ </tr>
795
+ </tbody>
796
+ </table>
797
+
798
+ <div class="docs-warning">
799
+ <strong>⚠️ Strip thought tags!</strong><br>
800
+ Many reasoning models wrap chain-of-thought in
801
+ <span class="docs-inline-code">&lt;think&gt;…&lt;/think&gt;</span> or
802
+ <span class="docs-inline-code">&lt;thinking&gt;…&lt;/thinking&gt;</span> blocks.
803
+ These confuse the classifier. The API strips them automatically, but you should
804
+ remove them on your side too to save bandwidth.
805
+ </div>
806
+ </div>
807
+
808
+ <!-- ── Response ── -->
809
+ <div class="docs-section">
810
+ <h2>Response</h2>
811
+ <div class="docs-code-block">
812
+ <div class="docs-code-header">
813
+ <span>JSON</span>
814
+ <button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
815
+ </div>
816
+ <pre>{
817
+ "provider": "Anthropic",
818
+ "confidence": 87.42,
819
+ "top_providers": [
820
+ { "name": "Anthropic", "confidence": 87.42 },
821
+ { "name": "OpenAI", "confidence": 6.15 },
822
+ { "name": "Google", "confidence": 3.28 },
823
+ { "name": "xAI", "confidence": 1.74 },
824
+ { "name": "DeepSeek", "confidence": 0.89 }
825
+ ]
826
+ }</pre>
827
+ </div>
828
+
829
+ <table class="docs-table">
830
+ <thead>
831
+ <tr><th>Field</th><th>Type</th><th>Description</th></tr>
832
+ </thead>
833
+ <tbody>
834
+ <tr>
835
+ <td><code>provider</code></td>
836
+ <td>string</td>
837
+ <td>Best-matching provider name</td>
838
+ </tr>
839
+ <tr>
840
+ <td><code>confidence</code></td>
841
+ <td>float</td>
842
+ <td>Confidence % for the top provider</td>
843
+ </tr>
844
+ <tr>
845
+ <td><code>top_providers</code></td>
846
+ <td>array</td>
847
+ <td>Ranked list of <code>{ name, confidence }</code> objects</td>
848
+ </tr>
849
+ </tbody>
850
+ </table>
851
+ </div>
852
+
853
+ <!-- ── Errors ── -->
854
+ <div class="docs-section">
855
+ <h2>Errors</h2>
856
+ <table class="docs-table">
857
+ <thead>
858
+ <tr><th>Status</th><th>Meaning</th></tr>
859
+ </thead>
860
+ <tbody>
861
+ <tr><td><code>400</code></td><td>Missing <code>text</code> field or text shorter than 20 characters</td></tr>
862
+ <tr><td><code>429</code></td><td>Rate limit exceeded (60 requests/minute per IP)</td></tr>
863
+ </tbody>
864
+ </table>
865
+ </div>
866
+
867
+ <!-- ── Code Examples ── -->
868
+ <div class="docs-section">
869
+ <h2>Code Examples</h2>
870
+
871
+ <h3>cURL</h3>
872
+ <div class="docs-code-block">
873
+ <div class="docs-code-header">
874
+ <span>Bash</span>
875
+ <button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
876
+ </div>
877
+ <pre>curl -X POST https://huggingface.co/spaces/CompactAI/AIFinder/v1/classify \
878
+ -H "Content-Type: application/json" \
879
+ -d '{
880
+ "text": "I would be happy to help you with that! Here is a detailed explanation of how neural networks work...",
881
+ "top_n": 5
882
+ }'</pre>
883
+ </div>
884
+
885
+ <h3>Python</h3>
886
+ <div class="docs-code-block">
887
+ <div class="docs-code-header">
888
+ <span>Python</span>
889
+ <button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
890
+ </div>
891
+ <pre>import re
892
+ import requests
893
+
894
+ API_URL = "https://huggingface.co/spaces/CompactAI/AIFinder/v1/classify"
895
+
896
+ def strip_think_tags(text):
897
+ """Remove &lt;think&gt;/&lt;thinking&gt; blocks before classifying."""
898
+ return re.sub(r"&lt;think(?:ing)?&gt;.*?&lt;/think(?:ing)?&gt;",
899
+ "", text, flags=re.DOTALL).strip()
900
+
901
+ text = """I'd be happy to help! Neural networks are
902
+ computational models inspired by the human brain..."""
903
+
904
+ # Strip thought tags first (the API does this too,
905
+ # but saves bandwidth to do it client-side)
906
+ cleaned = strip_think_tags(text)
907
+
908
+ response = requests.post(API_URL, json={
909
+ "text": cleaned,
910
+ "top_n": 5
911
+ })
912
+
913
+ data = response.json()
914
+ print(f"Provider: {data['provider']} ({data['confidence']:.1f}%)")
915
+ for p in data["top_providers"]:
916
+ print(f" {p['name']:&lt;20s} {p['confidence']:5.1f}%")</pre>
917
+ </div>
918
+
919
+ <h3>JavaScript (fetch)</h3>
920
+ <div class="docs-code-block">
921
+ <div class="docs-code-header">
922
+ <span>JavaScript</span>
923
+ <button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
924
+ </div>
925
+ <pre>const API_URL = "https://huggingface.co/spaces/CompactAI/AIFinder/v1/classify";
926
+
927
+ function stripThinkTags(text) {
928
+ return text.replace(/&lt;think(?:ing)?&gt;[\s\S]*?&lt;\/think(?:ing)?&gt;/g, "").trim();
929
+ }
930
+
931
+ async function classify(text, topN = 5) {
932
+ const cleaned = stripThinkTags(text);
933
+ const res = await fetch(API_URL, {
934
+ method: "POST",
935
+ headers: { "Content-Type": "application/json" },
936
+ body: JSON.stringify({ text: cleaned, top_n: topN })
937
+ });
938
+ return res.json();
939
+ }
940
+
941
+ // Usage
942
+ classify("I'd be happy to help you understand...")
943
+ .then(data =&gt; {
944
+ console.log(`Provider: ${data.provider} (${data.confidence}%)`);
945
+ data.top_providers.forEach(p =&gt;
946
+ console.log(` ${p.name}: ${p.confidence}%`)
947
+ );
948
+ });</pre>
949
+ </div>
950
+
951
+ <h3>Node.js</h3>
952
+ <div class="docs-code-block">
953
+ <div class="docs-code-header">
954
+ <span>JavaScript (Node)</span>
955
+ <button class="docs-copy-btn" onclick="copyCode(this)">Copy</button>
956
+ </div>
957
+ <pre>const API_URL = "https://huggingface.co/spaces/CompactAI/AIFinder/v1/classify";
958
+
959
+ async function classify(text, topN = 5) {
960
+ const cleaned = text
961
+ .replace(/&lt;think(?:ing)?&gt;[\s\S]*?&lt;\/think(?:ing)?&gt;/g, "")
962
+ .trim();
963
+
964
+ const res = await fetch(API_URL, {
965
+ method: "POST",
966
+ headers: { "Content-Type": "application/json" },
967
+ body: JSON.stringify({ text: cleaned, top_n: topN })
968
+ });
969
+
970
+ if (!res.ok) {
971
+ const err = await res.json();
972
+ throw new Error(err.error || `HTTP ${res.status}`);
973
+ }
974
+ return res.json();
975
+ }
976
+
977
+ // Example
978
+ (async () =&gt; {
979
+ const result = await classify(
980
+ "Let me think about this step by step...",
981
+ 3
982
+ );
983
+ console.log(result);
984
+ })();</pre>
985
+ </div>
986
+ </div>
987
+
988
+ <!-- ── Try It ── -->
989
+ <div class="docs-section">
990
+ <h2>Try It</h2>
991
+ <p>Test the API right here β€” paste any AI-generated text and hit Send.</p>
992
+ <div class="docs-try-it">
993
+ <textarea id="docsTestInput" placeholder="Paste AI-generated text here..."></textarea>
994
+ <div class="btn-group">
995
+ <button class="btn btn-primary" id="docsTestBtn">Send Request</button>
996
+ </div>
997
+ <div class="docs-try-output" id="docsTestOutput"></div>
998
+ </div>
999
+ </div>
1000
+
1001
+ <!-- ── Providers ── -->
1002
+ <div class="docs-section">
1003
+ <h2>Supported Providers</h2>
1004
+ <p>The classifier currently supports these providers:</p>
1005
+ <div id="docsProviderList" style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem;"></div>
1006
+ </div>
1007
+ </div>
1008
+
1009
+ <div class="footer">
1010
+ <p>AIFinder &mdash; Train on corrections to improve accuracy</p>
1011
+ <p style="margin-top: 0.5rem;">
1012
+ Want to contribute? Test this and post to the
1013
+ <a href="https://huggingface.co/spaces" target="_blank">HuggingFace Spaces Community</a>
1014
+ if you want it merged!
1015
+ </p>
1016
+ </div>
1017
+ </div>
1018
+
1019
+ <div class="toast" id="toast"></div>
1020
+
1021
+ <script>
1022
+ const API_BASE = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
1023
+ ? 'http://localhost:7860'
1024
+ : '';
1025
+
1026
+ let providers = [];
1027
+ let correctionsCount = 0;
1028
+ let sessionCorrections = 0;
1029
+
1030
+ const inputText = document.getElementById('inputText');
1031
+ const classifyBtn = document.getElementById('classifyBtn');
1032
+ const classifyBtnText = document.getElementById('classifyBtnText');
1033
+ const clearBtn = document.getElementById('clearBtn');
1034
+ const results = document.getElementById('results');
1035
+ const resultProvider = document.getElementById('resultProvider');
1036
+ const resultConfidence = document.getElementById('resultConfidence');
1037
+ const resultBar = document.getElementById('resultBar');
1038
+ const resultList = document.getElementById('resultList');
1039
+ const correction = document.getElementById('correction');
1040
+ const providerSelect = document.getElementById('providerSelect');
1041
+ const trainBtn = document.getElementById('trainBtn');
1042
+ const stats = document.getElementById('stats');
1043
+ const correctionsCountEl = document.getElementById('correctionsCount');
1044
+ const sessionCountEl = document.getElementById('sessionCount');
1045
+ const actions = document.getElementById('actions');
1046
+ const exportBtn = document.getElementById('exportBtn');
1047
+ const communityBtn = document.getElementById('communityBtn');
1048
+ const communityWarning = document.getElementById('communityWarning');
1049
+ const resetBtn = document.getElementById('resetBtn');
1050
+ const toast = document.getElementById('toast');
1051
+ const statusDot = document.getElementById('statusDot');
1052
+ const statusText = document.getElementById('statusText');
1053
+ let usingCommunity = false;
1054
+
1055
+ function showToast(message, type = 'info') {
1056
+ toast.textContent = message;
1057
+ toast.className = 'toast visible' + (type === 'success' ? ' success' : '');
1058
+ setTimeout(() => {
1059
+ toast.classList.remove('visible');
1060
+ }, 3000);
1061
+ }
1062
+
1063
+ async function checkStatus() {
1064
+ try {
1065
+ const res = await fetch(`${API_BASE}/api/status`);
1066
+ const data = await res.json();
1067
+ if (data.loaded) {
1068
+ statusDot.classList.remove('loading');
1069
+ statusText.textContent = data.using_community ? 'Ready β€” Community Model (cpu)' : `Ready (${data.device})`;
1070
+ classifyBtn.disabled = false;
1071
+ usingCommunity = data.using_community;
1072
+ updateCommunityUI(data.community_available);
1073
+ if (data.corrections_count > 0) {
1074
+ correctionsCount = data.corrections_count;
1075
+ correctionsCountEl.textContent = correctionsCount;
1076
+ stats.style.display = 'flex';
1077
+ actions.style.display = 'flex';
1078
+ }
1079
+ loadProviders();
1080
+ loadStats();
1081
+ } else {
1082
+ setTimeout(checkStatus, 1000);
1083
+ }
1084
+ } catch (e) {
1085
+ statusDot.classList.add('loading');
1086
+ statusText.textContent = 'Connecting to API...';
1087
+ setTimeout(checkStatus, 2000);
1088
+ }
1089
+ }
1090
+
1091
+ function updateCommunityUI(available) {
1092
+ if (available) {
1093
+ communityBtn.style.display = '';
1094
+ communityBtn.textContent = usingCommunity ? 'Use Official Model' : 'Use Community Model';
1095
+ communityWarning.style.display = usingCommunity ? 'block' : 'none';
1096
+ actions.style.display = 'flex';
1097
+ } else {
1098
+ communityBtn.style.display = 'none';
1099
+ communityWarning.style.display = 'none';
1100
+ }
1101
+ }
1102
+
1103
+ async function loadProviders() {
1104
+ const res = await fetch(`${API_BASE}/api/providers`);
1105
+ const data = await res.json();
1106
+ providers = data.providers;
1107
+
1108
+ providerSelect.innerHTML = providers.map(p =>
1109
+ `<option value="${p}">${p}</option>`
1110
+ ).join('');
1111
+ }
1112
+
1113
+ function loadStats() {
1114
+ const saved = localStorage.getItem('aifinder_corrections');
1115
+ if (saved) {
1116
+ correctionsCount = parseInt(saved, 10);
1117
+ correctionsCountEl.textContent = correctionsCount;
1118
+ stats.style.display = 'flex';
1119
+ actions.style.display = 'flex';
1120
+ }
1121
+ sessionCountEl.textContent = sessionCorrections;
1122
+ }
1123
+
1124
+ function saveStats() {
1125
+ localStorage.setItem('aifinder_corrections', correctionsCount.toString());
1126
+ }
1127
+
1128
+ async function classify() {
1129
+ const text = inputText.value.trim();
1130
+ if (text.length < 20) {
1131
+ showToast('Text must be at least 20 characters');
1132
+ return;
1133
+ }
1134
+
1135
+ classifyBtn.disabled = true;
1136
+ classifyBtnText.innerHTML = '<span class="loading"></span>';
1137
+
1138
+ try {
1139
+ const res = await fetch(`${API_BASE}/api/classify`, {
1140
+ method: 'POST',
1141
+ headers: { 'Content-Type': 'application/json' },
1142
+ body: JSON.stringify({ text })
1143
+ });
1144
+
1145
+ if (!res.ok) {
1146
+ throw new Error('Classification failed');
1147
+ }
1148
+
1149
+ const data = await res.json();
1150
+ showResults(data);
1151
+ } catch (e) {
1152
+ showToast('Error: ' + e.message);
1153
+ } finally {
1154
+ classifyBtn.disabled = false;
1155
+ classifyBtnText.textContent = 'Classify';
1156
+ }
1157
+ }
1158
+
1159
+ function showResults(data) {
1160
+ resultProvider.textContent = data.provider;
1161
+ resultConfidence.textContent = data.confidence.toFixed(1) + '%';
1162
+ resultBar.style.width = data.confidence + '%';
1163
+
1164
+ resultList.innerHTML = data.top_providers.map(p => `
1165
+ <li class="result-item">
1166
+ <span class="result-name">${p.name}</span>
1167
+ <span class="result-percent">${p.confidence.toFixed(1)}%</span>
1168
+ </li>
1169
+ `).join('');
1170
+
1171
+ providerSelect.value = data.provider;
1172
+
1173
+ results.classList.add('visible');
1174
+ correction.classList.add('visible');
1175
+
1176
+ if (correctionsCount > 0 || sessionCorrections > 0) {
1177
+ stats.style.display = 'flex';
1178
+ actions.style.display = 'flex';
1179
+ }
1180
+ }
1181
+
1182
+ async function train() {
1183
+ const text = inputText.value.trim();
1184
+ const correctProvider = providerSelect.value;
1185
+
1186
+ trainBtn.disabled = true;
1187
+ trainBtn.innerHTML = '<span class="loading"></span>';
1188
+
1189
+ try {
1190
+ const res = await fetch(`${API_BASE}/api/correct`, {
1191
+ method: 'POST',
1192
+ headers: { 'Content-Type': 'application/json' },
1193
+ body: JSON.stringify({ text, correct_provider: correctProvider })
1194
+ });
1195
+
1196
+ if (!res.ok) {
1197
+ throw new Error('Training failed');
1198
+ }
1199
+
1200
+ const data = await res.json();
1201
+ correctionsCount = data.corrections || correctionsCount + 1;
1202
+ sessionCorrections++;
1203
+ saveStats();
1204
+ correctionsCountEl.textContent = correctionsCount;
1205
+ sessionCountEl.textContent = sessionCorrections;
1206
+
1207
+ showToast('Correction saved & community model retrained!', 'success');
1208
+
1209
+ stats.style.display = 'flex';
1210
+ actions.style.display = 'flex';
1211
+ updateCommunityUI(true);
1212
+
1213
+ classify();
1214
+ } catch (e) {
1215
+ showToast('Error: ' + e.message);
1216
+ } finally {
1217
+ trainBtn.disabled = false;
1218
+ trainBtn.textContent = 'Train & Save';
1219
+ }
1220
+ }
1221
+
1222
+ async function exportModel() {
1223
+ exportBtn.disabled = true;
1224
+ exportBtn.innerHTML = '<span class="loading"></span>';
1225
+
1226
+ try {
1227
+ const res = await fetch(`${API_BASE}/api/save`, {
1228
+ method: 'POST',
1229
+ headers: { 'Content-Type': 'application/json' },
1230
+ body: JSON.stringify({ filename: 'aifinder_trained.pt' })
1231
+ });
1232
+
1233
+ if (!res.ok) {
1234
+ throw new Error('Save failed');
1235
+ }
1236
+
1237
+ const data = await res.json();
1238
+
1239
+ const link = document.createElement('a');
1240
+ link.href = `${API_BASE}/models/${data.filename}`;
1241
+ link.download = data.filename;
1242
+ link.click();
1243
+
1244
+ showToast('Model exported!', 'success');
1245
+ } catch (e) {
1246
+ showToast('Error: ' + e.message);
1247
+ } finally {
1248
+ exportBtn.disabled = false;
1249
+ exportBtn.textContent = 'Export Trained Model';
1250
+ }
1251
+ }
1252
+
1253
+ function resetTraining() {
1254
+ if (!confirm('Reset all training data? This cannot be undone.')) {
1255
+ return;
1256
+ }
1257
+
1258
+ correctionsCount = 0;
1259
+ sessionCorrections = 0;
1260
+ localStorage.removeItem('aifinder_corrections');
1261
+ correctionsCountEl.textContent = '0';
1262
+ sessionCountEl.textContent = '0';
1263
+ stats.style.display = 'none';
1264
+ actions.style.display = 'none';
1265
+ showToast('Training data reset');
1266
+ }
1267
+
1268
+ classifyBtn.addEventListener('click', classify);
1269
+ clearBtn.addEventListener('click', () => {
1270
+ inputText.value = '';
1271
+ results.classList.remove('visible');
1272
+ correction.classList.remove('visible');
1273
+ });
1274
+ trainBtn.addEventListener('click', train);
1275
+ exportBtn.addEventListener('click', exportModel);
1276
+ resetBtn.addEventListener('click', resetTraining);
1277
+ communityBtn.addEventListener('click', async () => {
1278
+ communityBtn.disabled = true;
1279
+ try {
1280
+ const res = await fetch(`${API_BASE}/api/toggle_community`, {
1281
+ method: 'POST',
1282
+ headers: { 'Content-Type': 'application/json' },
1283
+ body: JSON.stringify({ enabled: !usingCommunity })
1284
+ });
1285
+ const data = await res.json();
1286
+ usingCommunity = data.using_community;
1287
+ updateCommunityUI(data.available);
1288
+ statusText.textContent = usingCommunity ? 'Ready β€” Community Model (cpu)' : 'Ready (cpu)';
1289
+ showToast(usingCommunity ? 'Switched to community model' : 'Switched to official model', 'success');
1290
+ } catch (e) {
1291
+ showToast('Error: ' + e.message);
1292
+ } finally {
1293
+ communityBtn.disabled = false;
1294
+ }
1295
+ });
1296
+
1297
+ inputText.addEventListener('keydown', (e) => {
1298
+ if (e.key === 'Enter' && e.ctrlKey) {
1299
+ classify();
1300
+ }
1301
+ });
1302
+
1303
+ // ── Tab switching ──
1304
+ document.querySelectorAll('.tab').forEach(tab => {
1305
+ tab.addEventListener('click', () => {
1306
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1307
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
1308
+ tab.classList.add('active');
1309
+ document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
1310
+ });
1311
+ });
1312
+
1313
+ // ── Copy button for code blocks ──
1314
+ function copyCode(btn) {
1315
+ const pre = btn.closest('.docs-code-block').querySelector('pre');
1316
+ navigator.clipboard.writeText(pre.textContent).then(() => {
1317
+ btn.textContent = 'Copied!';
1318
+ setTimeout(() => { btn.textContent = 'Copy'; }, 1500);
1319
+ });
1320
+ }
1321
+
1322
+ // ── Docs: populate provider badges ──
1323
+ function populateDocsProviders() {
1324
+ const list = document.getElementById('docsProviderList');
1325
+ if (!list || !providers.length) return;
1326
+ list.innerHTML = providers.map(p =>
1327
+ `<span class="docs-inline-code" style="padding:0.3rem 0.75rem;">${p}</span>`
1328
+ ).join('');
1329
+ }
1330
+
1331
+ // ── Docs: "Try It" live tester ──
1332
+ const docsTestBtn = document.getElementById('docsTestBtn');
1333
+ const docsTestInput = document.getElementById('docsTestInput');
1334
+ const docsTestOutput = document.getElementById('docsTestOutput');
1335
+
1336
+ if (docsTestBtn) {
1337
+ docsTestBtn.addEventListener('click', async () => {
1338
+ const text = docsTestInput.value.trim();
1339
+ if (text.length < 20) {
1340
+ docsTestOutput.textContent = '{"error": "Text too short (minimum 20 characters)"}';
1341
+ docsTestOutput.classList.add('visible');
1342
+ return;
1343
+ }
1344
+ docsTestBtn.disabled = true;
1345
+ docsTestBtn.innerHTML = '<span class="loading"></span>';
1346
+ try {
1347
+ const res = await fetch(`${API_BASE}/v1/classify`, {
1348
+ method: 'POST',
1349
+ headers: { 'Content-Type': 'application/json' },
1350
+ body: JSON.stringify({ text, top_n: 5 })
1351
+ });
1352
+ const data = await res.json();
1353
+ docsTestOutput.textContent = JSON.stringify(data, null, 2);
1354
+ } catch (e) {
1355
+ docsTestOutput.textContent = `{"error": "${e.message}"}`;
1356
+ }
1357
+ docsTestOutput.classList.add('visible');
1358
+ docsTestBtn.disabled = false;
1359
+ docsTestBtn.textContent = 'Send Request';
1360
+ });
1361
+ }
1362
+
1363
+ // Hook provider list population into the existing load flow
1364
+ const _origLoadProviders = loadProviders;
1365
+ loadProviders = async function() {
1366
+ await _origLoadProviders();
1367
+ populateDocsProviders();
1368
+ };
1369
+
1370
+ checkStatus();
1371
+ </script>
1372
+ </body>
1373
+ </html>