gradio server

#1
by akhaliq HF Staff - opened
Files changed (2) hide show
  1. app.py +103 -601
  2. index.html +1830 -0
app.py CHANGED
@@ -3,6 +3,7 @@
3
  from __future__ import annotations
4
 
5
  import html
 
6
  import logging
7
  import os
8
  import re
@@ -15,7 +16,8 @@ from dataclasses import dataclass
15
  from pathlib import Path
16
  from typing import Any
17
 
18
- import gradio as gr
 
19
  from cohere import ClientV2
20
  from cohere.core.api_error import ApiError
21
 
@@ -345,8 +347,8 @@ def _targeted_prompt(
345
  "If the user asks a coding question or wants reasoning that does not require running code, "
346
  "answer directly without a fenced block. If they ask to generate, revise, or fix runnable "
347
  "web code, return one ```html fenced block only. The code is rendered inside a sandboxed "
348
- "iframe that spans the full width of the preview panel and is about 680px tall, so design "
349
- "the page to fill that iframe responsively: html/body at margin:0 and 100% width/height, "
350
  "avoid fixed widths larger than the iframe, and resize any <canvas> to its container "
351
  "(including on window resize) so the whole app is visible without horizontal scrolling."
352
  f"{context_block}\n\n"
@@ -560,123 +562,7 @@ def build_iframe(code: str, fence_lang: str | None = None) -> str:
560
  )
561
 
562
 
563
- TAB_PREVIEW = "preview"
564
- TAB_CONSOLE = "console"
565
- TAB_CODE = "code"
566
-
567
-
568
- def _status_badge(text: str, state: str = "idle") -> str:
569
- spinner = '<span class="status-spinner"></span>' if state == "working" else '<span class="status-dot"></span>'
570
- return (
571
- f'<div class="status-badge status-{state}">{spinner}'
572
- f"<span>{html.escape(text)}</span></div>"
573
- )
574
-
575
-
576
- def _preview_placeholder(message: str = "Run code to see the result here.") -> str:
577
- return (
578
- '<div class="preview-placeholder">'
579
- '<div class="pp-emoji">🖼️</div>'
580
- f"<strong>{html.escape(message)}</strong>"
581
- "<span>A matplotlib figure or a live web preview will appear in this panel.</span>"
582
- "</div>"
583
- )
584
-
585
-
586
- def _write_download(code: str, language: str) -> str | None:
587
- if not code:
588
- return None
589
- ext = "py" if language == "python" else "html"
590
- try:
591
- directory = tempfile.mkdtemp(prefix="coding_model_dl_")
592
- path = Path(directory) / f"generated.{ext}"
593
- path.write_text(code, encoding="utf-8")
594
- return str(path)
595
- except Exception:
596
- logger.warning("Could not write downloadable snippet", exc_info=True)
597
- return None
598
-
599
-
600
- def _display_kwargs(execution_context: dict[str, Any] | None) -> dict[str, Any]:
601
- """Map the persisted execution context onto concrete component updates."""
602
-
603
- base = {
604
- "show_image": False,
605
- "image": None,
606
- "show_web": False,
607
- "web_html": "",
608
- "show_placeholder": True,
609
- "placeholder_msg": "Run code to see the result here.",
610
- "stdout": "",
611
- "stderr": "",
612
- "code": "",
613
- "code_language": "python",
614
- "download_path": None,
615
- "suggested_tab": None,
616
- }
617
- if not execution_context or not execution_context.get("code"):
618
- return base
619
-
620
- target = str(execution_context.get("target") or "")
621
- code = str(execution_context.get("code") or "")
622
- fence_lang = str(execution_context.get("fence_lang") or target)
623
- download = execution_context.get("download_path")
624
-
625
- if target == "web":
626
- base.update(
627
- show_web=True,
628
- web_html=build_iframe(code, fence_lang),
629
- show_placeholder=False,
630
- code=code,
631
- code_language="html",
632
- download_path=download,
633
- suggested_tab=TAB_PREVIEW,
634
- )
635
- return base
636
-
637
- image = execution_context.get("image_path")
638
- base.update(
639
- show_image=bool(image),
640
- image=image,
641
- show_placeholder=not bool(image),
642
- placeholder_msg="No figure produced — open the Console tab for program output.",
643
- stdout=str(execution_context.get("stdout") or ""),
644
- stderr=str(execution_context.get("stderr") or ""),
645
- code=code,
646
- code_language="python",
647
- download_path=download,
648
- suggested_tab=TAB_PREVIEW if image else TAB_CONSOLE,
649
- )
650
- return base
651
-
652
-
653
- def _output_state(
654
- history: list[dict[str, str]],
655
- prompt_value: str,
656
- status_text: str,
657
- status_state: str,
658
- execution_context: dict[str, Any] | None,
659
- *,
660
- selected_tab: str | None = None,
661
- display: dict[str, Any] | None = None,
662
- ) -> tuple[Any, ...]:
663
- data = display if display is not None else _display_kwargs(execution_context)
664
- tab_update = gr.update(selected=selected_tab) if selected_tab else gr.update()
665
- return (
666
- history,
667
- prompt_value,
668
- _status_badge(status_text, status_state),
669
- tab_update,
670
- gr.update(value=_preview_placeholder(data["placeholder_msg"]), visible=data["show_placeholder"]),
671
- gr.update(value=data["image"], visible=data["show_image"]),
672
- gr.update(value=data["web_html"], visible=data["show_web"]),
673
- gr.update(visible=data["show_web"]),
674
- data["stdout"],
675
- data["stderr"],
676
- gr.update(value=data["code"], language=data["code_language"]),
677
- gr.update(value=data["download_path"], visible=bool(data["download_path"])),
678
- execution_context or {},
679
- )
680
 
681
 
682
  def _run_extracted_code(
@@ -719,509 +605,125 @@ def _updated_execution_context(
719
  }
720
 
721
 
722
- def handle_submit(
723
- prompt: str,
724
- target_language: str,
725
- history: list[dict[str, str]] | None,
726
- execution_context: dict[str, Any] | None,
727
- ) -> Iterator[tuple[Any, ...]]:
728
- history = list(history or [])
729
- execution_context = execution_context or {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
  prompt = (prompt or "").strip()
731
  if not prompt:
732
- yield _output_state(history, "", "Enter a prompt to get started.", "info", execution_context)
733
  return
734
 
735
- generation_history = history + [
 
736
  {"role": "user", "content": prompt},
737
  {"role": "assistant", "content": ""},
738
  ]
739
- assistant_index = len(generation_history) - 1
740
- yield _output_state(generation_history, "", "Thinking…", "working", execution_context)
741
 
742
- cohere_history = history + [
743
- {"role": "user", "content": _targeted_prompt(prompt, target_language, execution_context)}
744
- ]
 
745
  messages = _chat_history_to_messages(cohere_history)
746
 
747
  final_response = ""
748
  for partial in call_model(messages):
749
  final_response = partial
750
- generation_history[assistant_index]["content"] = partial
751
- yield _output_state(generation_history, "", "Generating…", "working", execution_context)
752
 
753
  if not final_response:
754
- generation_history[assistant_index]["content"] = "The model did not return a response."
755
- yield _output_state(generation_history, "", "No model response.", "error", execution_context)
756
  return
757
 
758
  code, fence_lang = extract_code(final_response)
759
  target = _normalize_language(target_language, fence_lang)
 
760
  if not code:
761
- yield _output_state(generation_history, "", "Answered without running code.", "info", execution_context)
762
  return
763
 
764
- yield _output_state(generation_history, "", "Running…", "working", execution_context)
765
-
766
- stdout, stderr, image, status_text, status_state = _run_extracted_code(code, target)
767
- download = _write_download(code, "python" if target == "python" else "html")
768
- execution_context = _updated_execution_context(
769
- code=code,
770
- target=target,
771
- fence_lang=fence_lang,
772
- stdout=stdout,
773
- stderr=stderr,
774
- image_path=image,
775
- status=status_text,
776
- download_path=download,
777
- )
778
- data = _display_kwargs(execution_context)
779
- yield _output_state(
780
- generation_history,
781
- "",
782
- status_text,
783
- status_state,
784
- execution_context,
785
- selected_tab=data["suggested_tab"],
786
- display=data,
787
- )
788
-
789
-
790
- def _make_example_runner(prompt: str, target: str):
791
- def runner(*_ignored: Any) -> Iterator[tuple[Any, ...]]:
792
- # Starters always begin a fresh conversation rather than appending to the
793
- # current thread, so prior history and execution context are discarded.
794
- yield from handle_submit(prompt, target, [], {})
795
-
796
- return runner
797
-
798
-
799
- def start_new_conversation() -> tuple[Any, ...]:
800
- return _output_state(
801
- [],
802
- "",
803
- "New conversation — ask anything about code.",
804
- "idle",
805
- {},
806
- selected_tab=TAB_PREVIEW,
807
- )
808
-
809
-
810
- def _cancelled_status() -> str:
811
- return _status_badge("Stopped.", "info")
812
-
813
-
814
- APP_THEME = gr.themes.Soft(
815
- primary_hue="indigo",
816
- secondary_hue="violet",
817
- neutral_hue="slate",
818
- radius_size=gr.themes.sizes.radius_lg,
819
- font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
820
- font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "SFMono-Regular", "monospace"],
821
- ).set(
822
- body_background_fill="*neutral_50",
823
- body_background_fill_dark="*neutral_950",
824
- block_shadow="0 1px 2px rgba(16,24,40,.04), 0 10px 28px -18px rgba(16,24,40,.22)",
825
- button_primary_shadow="0 8px 22px -10px rgba(99,102,241,.7)",
826
- )
827
-
828
- CUSTOM_CSS = """
829
- .gradio-container { max-width: 1720px !important; margin: 0 auto !important; padding-left: 1.25rem !important; padding-right: 1.25rem !important; }
830
-
831
- /* ---------- Hero ---------- */
832
- .hero { padding: 1.2rem 0 0.2rem; }
833
- .hero-row { display: flex; align-items: center; gap: 0.95rem; flex-wrap: wrap; }
834
- .hero-mark {
835
- width: 54px; height: 54px; border-radius: 16px; flex: none;
836
- background: linear-gradient(135deg, #3b82f6, #6366f1 45%, #8b5cf6);
837
- display: flex; align-items: center; justify-content: center;
838
- font-size: 1.75rem; box-shadow: 0 10px 26px -10px rgba(99,102,241,.7);
839
- }
840
- .hero h1 {
841
- font-size: 1.95rem; font-weight: 800; line-height: 1.1; margin: 0;
842
- letter-spacing: -0.025em;
843
- background: linear-gradient(110deg, #2563eb, #7c3aed 55%, #9333ea);
844
- -webkit-background-clip: text; background-clip: text; color: transparent;
845
- }
846
- .hero .subtitle { margin: 0.28rem 0 0; color: var(--body-text-color-subdued); font-size: 1.02rem; }
847
- .hero-tip {
848
- margin: 0.7rem 0 0; padding: 0.5rem 0.8rem; font-size: 0.86rem; line-height: 1.45;
849
- display: flex; align-items: baseline; gap: 0.45rem;
850
- border: 1px solid var(--border-color-primary); border-left: 3px solid #7c3aed;
851
- border-radius: 12px; background: var(--block-background-fill);
852
- color: var(--body-text-color-subdued);
853
- }
854
- .hero-tip a { color: #6d28d9; font-weight: 600; text-decoration: none; white-space: nowrap; }
855
- .hero-tip a:hover { text-decoration: underline; }
856
- .dark .hero-tip a { color: #c4b5fd; }
857
- .hero-spacer { flex: 1 1 auto; min-width: 1rem; }
858
- .hero-pills { display: flex; flex-wrap: wrap; gap: 0.4rem; justify-content: flex-end; }
859
- .brand-pill {
860
- display: inline-flex; align-items: center; gap: 0.4rem;
861
- font-size: 0.78rem; font-weight: 600; padding: 0.34rem 0.7rem;
862
- border-radius: 999px; border: 1px solid var(--border-color-primary);
863
- background: var(--block-background-fill); color: var(--body-text-color-subdued);
864
- white-space: nowrap; text-decoration: none;
865
- transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease, color .12s ease;
866
- }
867
- a.brand-pill:hover {
868
- transform: translateY(-1px);
869
- border-color: #8b5cf6; color: var(--body-text-color);
870
- box-shadow: 0 8px 18px -12px rgba(124,58,237,.75);
871
- }
872
- .brand-pill .pill-ext { opacity: 0.55; font-size: 0.72rem; }
873
- .brand-pill.accent {
874
- border-color: transparent; color: #fff;
875
- background: linear-gradient(110deg, #4f46e5, #7c3aed);
876
- }
877
-
878
- /* ---------- Config alert ---------- */
879
- .config-alert {
880
- border: 1px solid #f4c7c7; background: #fdeeee; color: #8a1f1f;
881
- border-radius: 14px; padding: 0.75rem 0.95rem; font-size: 0.9rem; margin: 0.55rem 0 0;
882
- line-height: 1.45;
883
- }
884
- .config-alert code { background: rgba(0,0,0,.07); padding: 0.05rem 0.32rem; border-radius: 6px; }
885
- .dark .config-alert { background: rgba(180,35,24,.13); border-color: rgba(180,35,24,.42); color: #f3b4b4; }
886
- .dark .config-alert code { background: rgba(255,255,255,.1); }
887
-
888
- /* ---------- Panels ---------- */
889
- .panel-card {
890
- border: 1px solid var(--border-color-primary);
891
- border-radius: 20px; padding: 1.05rem 1.1rem 1.15rem;
892
- background: var(--block-background-fill);
893
- }
894
- .panel-head {
895
- display: flex; align-items: center; justify-content: space-between;
896
- gap: 0.6rem; margin-bottom: 0.7rem; min-height: 34px;
897
- }
898
- .panel-title { font-weight: 700; font-size: 1.05rem; display: flex; align-items: center; gap: 0.45rem; margin: 0; }
899
- .workbench-status { display: flex; justify-content: flex-end; }
900
-
901
- /* ---------- Status badge ---------- */
902
- .status-badge {
903
- display: inline-flex; align-items: center; gap: 0.5rem;
904
- font-size: 0.85rem; font-weight: 600; padding: 0.4rem 0.8rem;
905
- border-radius: 999px; border: 1px solid var(--border-color-primary);
906
- background: var(--block-background-fill); white-space: nowrap;
907
- }
908
- .status-dot { width: 9px; height: 9px; border-radius: 50%; background: #94a3b8; flex: none; }
909
- .status-info .status-dot { background: #3b82f6; }
910
- .status-success .status-dot { background: #22c55e; }
911
- .status-error .status-dot { background: #ef4444; }
912
- .status-error { border-color: rgba(239,68,68,.45); }
913
- .status-success { border-color: rgba(34,197,94,.45); }
914
- .status-working { border-color: rgba(99,102,241,.5); color: var(--body-text-color); }
915
- .status-spinner {
916
- width: 12px; height: 12px; border-radius: 50%; flex: none;
917
- border: 2px solid rgba(99,102,241,.3); border-top-color: #6366f1;
918
- animation: spin 0.7s linear infinite;
919
- }
920
- @keyframes spin { to { transform: rotate(360deg); } }
921
-
922
- /* ---------- Examples ---------- */
923
- .examples-label { font-size: 0.78rem; font-weight: 600; color: var(--body-text-color-subdued);
924
- text-transform: uppercase; letter-spacing: 0.05em; margin: 0.45rem 0 0.15rem; }
925
- .examples-wrap { flex-wrap: wrap !important; gap: 0.5rem !important; }
926
- button.example-chip {
927
- flex: 0 0 auto !important;
928
- border-radius: 999px !important;
929
- font-size: 0.83rem !important; font-weight: 600 !important;
930
- padding: 0.45rem 0.95rem !important;
931
- white-space: nowrap !important;
932
- width: auto !important; min-width: 0 !important;
933
- background: var(--block-background-fill) !important;
934
- border: 1px solid var(--border-color-primary) !important;
935
- transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease;
936
- }
937
- button.example-chip:hover {
938
- transform: translateY(-1px);
939
- border-color: #8b5cf6 !important;
940
- box-shadow: 0 8px 18px -12px rgba(124,58,237,.75);
941
- }
942
-
943
- /* ---------- Inputs ---------- */
944
- .send-btn button { font-weight: 700 !important; }
945
- .input-hint { font-size: 0.78rem; color: var(--body-text-color-subdued); margin: 0.2rem 0 0; }
946
-
947
- /* ---------- Target toggle ---------- */
948
- .target-toggle { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; }
949
-
950
- /* ---------- Console ---------- */
951
- .console-box textarea {
952
- font-family: var(--font-mono) !important;
953
- font-size: 0.82rem !important; line-height: 1.5 !important;
954
- }
955
- .stderr-box textarea { color: #b42318 !important; }
956
- .dark .stderr-box textarea { color: #fca5a5 !important; }
957
-
958
- /* ---------- Preview ---------- */
959
- .preview-placeholder {
960
- min-height: 560px;
961
- border: 1.5px dashed var(--border-color-primary);
962
- border-radius: 16px;
963
- display: flex; flex-direction: column; align-items: center; justify-content: center;
964
- gap: 0.45rem; color: var(--body-text-color-subdued); text-align: center; padding: 2rem;
965
- }
966
- .preview-placeholder .pp-emoji { font-size: 2.3rem; opacity: 0.85; }
967
- .preview-placeholder strong { font-size: 1.02rem; color: var(--body-text-color); }
968
- .web-frame { box-shadow: 0 8px 30px -18px rgba(16,24,40,.5); }
969
- .web-frame:fullscreen { border-radius: 0 !important; box-shadow: none !important; min-height: 100vh !important; background: #fff; }
970
- .preview-toolbar { justify-content: flex-end !important; margin-bottom: 0.5rem; }
971
- .fullscreen-btn button {
972
- border-radius: 999px !important;
973
- font-weight: 600 !important;
974
- white-space: nowrap !important;
975
- }
976
-
977
- /* ---------- Footer note ---------- */
978
- .safety-note {
979
- font-size: 0.78rem; color: var(--body-text-color-subdued);
980
- margin: 0.7rem 0 0; line-height: 1.45; display: flex; align-items: center; gap: 0.4rem;
981
- }
982
-
983
- /* ---------- Responsive ---------- */
984
- @media (max-width: 1024px) {
985
- .hero h1 { font-size: 1.6rem; }
986
- }
987
- """
988
-
989
-
990
- def _configuration_banner_html() -> str:
991
- messages: list[str] = []
992
- if not API_KEY_CONFIGURED:
993
- messages.append("Set <code>COHERE_API_KEY</code> as a Hugging Face Space secret.")
994
- if not messages:
995
- return ""
996
- items = " ".join(messages)
997
- return (
998
- '<div class="config-alert"><strong>⚙️ Configuration required.</strong> '
999
- f"{items} Generation stays disabled until this is done.</div>"
1000
- )
1001
-
1002
-
1003
- def _hero_html() -> str:
1004
- model_pill = (
1005
- f'<a class="brand-pill" href="{html.escape(MODEL_URL, quote=True)}" '
1006
- 'target="_blank" rel="noopener noreferrer" '
1007
- f'title="View {html.escape(MODEL_ID)} on Hugging Face">'
1008
- f'🧠 {html.escape(MODEL_ID)} <span class="pill-ext">↗</span></a>'
1009
- )
1010
- opencode_url = html.escape(OPENCODE_URL, quote=True)
1011
- return f"""
1012
- <section class="hero">
1013
- <div class="hero-row">
1014
- <div class="hero-mark">💻</div>
1015
- <div>
1016
- <h1>{html.escape(APP_TITLE)}</h1>
1017
- <p class="subtitle">Chat with a coding model. Run Python or Web code.</p>
1018
- </div>
1019
- <div class="hero-spacer"></div>
1020
- <div class="hero-pills">
1021
- {model_pill}
1022
- </div>
1023
- </div>
1024
- <p class="hero-tip">⌨️ <span><strong>North-Mini-Code-1.0</strong> is built for agentic coding and works best
1025
- in your terminal with <a href="{opencode_url}" target="_blank" rel="noopener noreferrer">OpenCode ↗</a>.
1026
- This Space is a browser playground for trying the model.</span></p>
1027
- </section>
1028
- {_configuration_banner_html()}
1029
- """
1030
-
1031
-
1032
- def build_demo() -> gr.Blocks:
1033
- with gr.Blocks(title=APP_TITLE, fill_height=True) as demo:
1034
- gr.HTML(_hero_html())
1035
-
1036
- execution_state = gr.State({})
1037
-
1038
- with gr.Row(equal_height=False):
1039
- # ----- Left: conversation -----
1040
- with gr.Column(scale=6, min_width=420, elem_classes=["panel-card"]):
1041
- with gr.Row(elem_classes=["panel-head"]):
1042
- gr.HTML('<div class="panel-title">💬 Conversation</div>')
1043
- new_conversation_btn = gr.Button(
1044
- "+ New chat", variant="secondary", size="sm", scale=0, min_width=120
1045
- )
1046
-
1047
- chatbot = gr.Chatbot(
1048
- label=None,
1049
- show_label=False,
1050
- height=440,
1051
- layout="bubble",
1052
- reasoning_tags=[("<think>", "</think>")],
1053
- placeholder=(
1054
- "### 👋 Ask me to write code\n"
1055
- "I can build runnable **Python** scripts and self-contained **web** demos.\n\n"
1056
- "Pick a starter below, or describe what you want to build."
1057
- ),
1058
- )
1059
-
1060
- gr.HTML('<p class="examples-label">Run generated code as</p>')
1061
- target_language = gr.Radio(
1062
- choices=["Python", "Web"],
1063
- value="Python",
1064
- show_label=False,
1065
- container=False,
1066
- interactive=True,
1067
- elem_classes=["target-toggle"],
1068
- )
1069
-
1070
- prompt = gr.Textbox(
1071
- label=None,
1072
- show_label=False,
1073
- placeholder="Ask a coding question, request code, or iterate on the previous result…",
1074
- lines=3,
1075
- max_lines=10,
1076
- submit_btn=False,
1077
- autofocus=True,
1078
- )
1079
- with gr.Row():
1080
- submit_btn = gr.Button("Send ➤", variant="primary", scale=4, elem_classes=["send-btn"])
1081
- stop_btn = gr.Button("Stop", variant="stop", scale=1, min_width=90)
1082
- gr.HTML('<p class="input-hint">Press <strong>Shift+Enter</strong> to send · <strong>Enter</strong> for a new line</p>')
1083
-
1084
- gr.HTML('<p class="examples-label">Try a starter</p>')
1085
- example_buttons: list[tuple[gr.Button, str, str]] = []
1086
- with gr.Row(elem_classes=["examples-wrap"]):
1087
- for label, ex_prompt, ex_target in EXAMPLE_PROMPTS:
1088
- btn = gr.Button(label, size="sm", scale=0, elem_classes=["example-chip"])
1089
- example_buttons.append((btn, ex_prompt, ex_target))
1090
-
1091
- # ----- Right: workbench -----
1092
- with gr.Column(scale=6, min_width=420, elem_classes=["panel-card"]):
1093
- with gr.Row(elem_classes=["panel-head"]):
1094
- gr.HTML('<div class="panel-title">🛠️ Workbench</div>')
1095
- status = gr.HTML(_status_badge("Idle.", "idle"), elem_classes=["workbench-status"])
1096
-
1097
- with gr.Tabs() as result_tabs:
1098
- with gr.Tab("Preview", id=TAB_PREVIEW):
1099
- preview_placeholder = gr.HTML(_preview_placeholder())
1100
- py_image = gr.Image(
1101
- label="Matplotlib figure",
1102
- type="filepath",
1103
- visible=False,
1104
- interactive=False,
1105
- show_label=False,
1106
- )
1107
- with gr.Row(elem_classes=["preview-toolbar"], visible=False) as preview_toolbar:
1108
- fullscreen_btn = gr.Button(
1109
- "⤢ Fullscreen",
1110
- size="sm",
1111
- scale=0,
1112
- min_width=120,
1113
- elem_classes=["fullscreen-btn"],
1114
- )
1115
- web_preview = gr.HTML(value="", visible=False, min_height=680)
1116
-
1117
- with gr.Tab("Console", id=TAB_CONSOLE):
1118
- py_stdout = gr.Textbox(
1119
- label="Program output (stdout)",
1120
- lines=11,
1121
- interactive=False,
1122
- elem_classes=["console-box"],
1123
- placeholder="Program output will appear here after a Python run.",
1124
- )
1125
- py_stderr = gr.Textbox(
1126
- label="Errors / stderr",
1127
- lines=7,
1128
- interactive=False,
1129
- elem_classes=["console-box", "stderr-box"],
1130
- placeholder="Tracebacks and error messages will appear here.",
1131
- )
1132
-
1133
- with gr.Tab("Code", id=TAB_CODE):
1134
- code_view = gr.Code(
1135
- label="Generated source",
1136
- language="python",
1137
- lines=20,
1138
- interactive=False,
1139
- wrap_lines=True,
1140
- )
1141
- download_btn = gr.DownloadButton(
1142
- "⬇ Download snippet", size="sm", visible=False
1143
- )
1144
-
1145
- outputs = [
1146
- chatbot,
1147
- prompt,
1148
- status,
1149
- result_tabs,
1150
- preview_placeholder,
1151
- py_image,
1152
- web_preview,
1153
- preview_toolbar,
1154
- py_stdout,
1155
- py_stderr,
1156
- code_view,
1157
- download_btn,
1158
- execution_state,
1159
- ]
1160
-
1161
- new_conversation_btn.click(
1162
- fn=start_new_conversation,
1163
- inputs=[],
1164
- outputs=outputs,
1165
- show_progress="hidden",
1166
- )
1167
-
1168
- submit_event = submit_btn.click(
1169
- fn=handle_submit,
1170
- inputs=[prompt, target_language, chatbot, execution_state],
1171
- outputs=outputs,
1172
- show_progress="minimal",
1173
- )
1174
- prompt_event = prompt.submit(
1175
- fn=handle_submit,
1176
- inputs=[prompt, target_language, chatbot, execution_state],
1177
- outputs=outputs,
1178
- show_progress="minimal",
1179
- )
1180
-
1181
- cancellable_events = [submit_event, prompt_event]
1182
- for btn, ex_prompt, ex_target in example_buttons:
1183
- set_target = btn.click(
1184
- fn=lambda t=ex_target: gr.update(value=t),
1185
- inputs=[],
1186
- outputs=[target_language],
1187
- show_progress="hidden",
1188
- )
1189
- run_event = set_target.then(
1190
- fn=_make_example_runner(ex_prompt, ex_target),
1191
- inputs=[chatbot, execution_state],
1192
- outputs=outputs,
1193
- show_progress="minimal",
1194
- )
1195
- cancellable_events.append(run_event)
1196
-
1197
- stop_btn.click(
1198
- fn=_cancelled_status,
1199
- inputs=[],
1200
- outputs=[status],
1201
- cancels=cancellable_events,
1202
- show_progress="hidden",
1203
- )
1204
-
1205
- # Expand the sandboxed web preview to fill the screen so users can actually
1206
- # interact with apps larger than the side panel. Runs purely client-side and
1207
- # keeps the iframe's sandbox attributes intact.
1208
- fullscreen_btn.click(
1209
- fn=None,
1210
- inputs=None,
1211
- outputs=None,
1212
- js=(
1213
- "() => { const f = document.querySelector('.web-frame'); "
1214
- "if (f) { const r = f.requestFullscreen || f.webkitRequestFullscreen; "
1215
- "if (r) { try { Promise.resolve(r.call(f)).catch(() => {}); } catch (e) {} } } }"
1216
- ),
1217
- )
1218
-
1219
- return demo
1220
-
1221
 
1222
- demo = build_demo()
1223
- demo.queue(default_concurrency_limit=2)
1224
 
1225
 
1226
- if __name__ == "__main__":
1227
- demo.launch(theme=APP_THEME, css=CUSTOM_CSS)
 
3
  from __future__ import annotations
4
 
5
  import html
6
+ import json
7
  import logging
8
  import os
9
  import re
 
16
  from pathlib import Path
17
  from typing import Any
18
 
19
+ from gradio import Server
20
+ from fastapi.responses import HTMLResponse, FileResponse
21
  from cohere import ClientV2
22
  from cohere.core.api_error import ApiError
23
 
 
347
  "If the user asks a coding question or wants reasoning that does not require running code, "
348
  "answer directly without a fenced block. If they ask to generate, revise, or fix runnable "
349
  "web code, return one ```html fenced block only. The code is rendered inside a sandboxed "
350
+ "iframe that fills the preview panel, so design the page to fill that iframe responsively: "
351
+ "html/body at margin:0 and 100% width/height, "
352
  "avoid fixed widths larger than the iframe, and resize any <canvas> to its container "
353
  "(including on window resize) so the whole app is visible without horizontal scrolling."
354
  f"{context_block}\n\n"
 
562
  )
563
 
564
 
565
+ # ---------- gradio.Server application ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
 
567
 
568
  def _run_extracted_code(
 
605
  }
606
 
607
 
608
+ # In-memory registry of temp files to serve (images, downloads)
609
+ _served_files: dict[str, str] = {}
610
+
611
+ app = Server()
612
+
613
+
614
+ @app.get("/", response_class=HTMLResponse)
615
+ async def homepage():
616
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
617
+ with open(html_path, "r", encoding="utf-8") as f:
618
+ content = f.read()
619
+ # Inject runtime config
620
+ config = json.dumps({
621
+ "api_key_configured": API_KEY_CONFIGURED,
622
+ "app_title": APP_TITLE,
623
+ "model_id": MODEL_ID,
624
+ "model_url": MODEL_URL,
625
+ "opencode_url": OPENCODE_URL,
626
+ "examples": [{"label": label, "prompt": prompt, "target": target} for label, prompt, target in EXAMPLE_PROMPTS],
627
+ })
628
+ content = content.replace("__RUNTIME_CONFIG__", config)
629
+ return content
630
+
631
+
632
+ @app.get("/images/{filename}")
633
+ async def serve_image(filename: str):
634
+ path = _served_files.get(f"img:{filename}")
635
+ if path and os.path.exists(path):
636
+ return FileResponse(path, media_type="image/png")
637
+ return HTMLResponse("Not found", status_code=404)
638
+
639
+
640
+ @app.get("/download/{filename}")
641
+ async def serve_download(filename: str):
642
+ path = _served_files.get(f"dl:{filename}")
643
+ if path and os.path.exists(path):
644
+ return FileResponse(path, filename=filename, media_type="application/octet-stream")
645
+ return HTMLResponse("Not found", status_code=404)
646
+
647
+
648
+ @app.api(name="chat", concurrency_limit=2)
649
+ def handle_chat(prompt: str, target_language: str, history_json: str, exec_context_json: str) -> str:
650
+ """Stream chat responses with code execution. Yields JSON strings."""
651
+ history = json.loads(history_json) if history_json else []
652
+ execution_context = json.loads(exec_context_json) if exec_context_json else {}
653
+
654
  prompt = (prompt or "").strip()
655
  if not prompt:
656
+ yield json.dumps({"type": "error", "status_text": "Enter a prompt to get started.", "status_state": "info", "history": history, "execution": execution_context})
657
  return
658
 
659
+ # Add user message and placeholder assistant message
660
+ history = list(history) + [
661
  {"role": "user", "content": prompt},
662
  {"role": "assistant", "content": ""},
663
  ]
664
+ yield json.dumps({"type": "status", "status_text": "Thinking…", "status_state": "working", "history": history, "execution": execution_context})
 
665
 
666
+ # Build messages for Cohere — use targeted prompt
667
+ cohere_history = list(history[:-1]) # everything except empty assistant
668
+ # Replace the last user message with the targeted version
669
+ cohere_history[-1] = {"role": "user", "content": _targeted_prompt(prompt, target_language, execution_context)}
670
  messages = _chat_history_to_messages(cohere_history)
671
 
672
  final_response = ""
673
  for partial in call_model(messages):
674
  final_response = partial
675
+ history[-1]["content"] = partial
676
+ yield json.dumps({"type": "streaming", "status_text": "Generating…", "status_state": "working", "history": history, "execution": execution_context})
677
 
678
  if not final_response:
679
+ history[-1]["content"] = "The model did not return a response."
680
+ yield json.dumps({"type": "error", "status_text": "No model response.", "status_state": "error", "history": history, "execution": execution_context})
681
  return
682
 
683
  code, fence_lang = extract_code(final_response)
684
  target = _normalize_language(target_language, fence_lang)
685
+
686
  if not code:
687
+ yield json.dumps({"type": "complete", "status_text": "Answered without running code.", "status_state": "info", "history": history, "execution": execution_context})
688
  return
689
 
690
+ yield json.dumps({"type": "status", "status_text": "Running…", "status_state": "working", "history": history, "execution": execution_context})
691
+
692
+ stdout, stderr, image_path, status_text, status_state = _run_extracted_code(code, target)
693
+
694
+ # Register image for serving
695
+ image_url = None
696
+ if image_path:
697
+ filename = os.path.basename(image_path)
698
+ _served_files[f"img:{filename}"] = image_path
699
+ image_url = f"/images/{filename}"
700
+
701
+ # Register code for download
702
+ download_url = None
703
+ if code:
704
+ ext = "py" if target == "python" else "html"
705
+ dl_filename = f"generated.{ext}"
706
+ dl_dir = tempfile.mkdtemp(prefix="coding_model_dl_")
707
+ dl_path = os.path.join(dl_dir, dl_filename)
708
+ Path(dl_path).write_text(code, encoding="utf-8")
709
+ _served_files[f"dl:{dl_filename}"] = dl_path
710
+ download_url = f"/download/{dl_filename}"
711
+
712
+ execution_context = {
713
+ "code": code,
714
+ "target": target,
715
+ "fence_lang": fence_lang or target,
716
+ "stdout": stdout,
717
+ "stderr": stderr,
718
+ "image_url": image_url,
719
+ "image_path": image_path,
720
+ "status": status_text,
721
+ "language": "python" if target == "python" else "html",
722
+ "suggested_tab": "preview" if (image_path or target == "web") else "console",
723
+ "download_url": download_url,
724
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
 
726
+ yield json.dumps({"type": "complete", "status_text": status_text, "status_state": status_state, "history": history, "execution": execution_context})
 
727
 
728
 
729
+ app.launch(show_error=True)
 
index.html ADDED
@@ -0,0 +1,1830 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>North Mini Code 1.0</title>
7
+ <meta name="description" content="Terminal-style coding assistant powered by North Mini Code 1.0">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ /* ═══════════════════════════════════════════════════════
13
+ RESET & BASE
14
+ ═══════════════════════════════════════════════════════ */
15
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
16
+
17
+ :root {
18
+ --bg-deep: #0a0e14;
19
+ --bg-panel: #0d1117;
20
+ --bg-code: #161b22;
21
+ --border: #1e2a3a;
22
+ --border-focus: #2d4a6a;
23
+ --green: #39ff14;
24
+ --green-dim: #1a7a0a;
25
+ --cyan: #00d4ff;
26
+ --amber: #ffb300;
27
+ --gray-light: #e0e0e0;
28
+ --gray-mid: #8b949e;
29
+ --gray-dim: #484f58;
30
+ --red: #ff5555;
31
+ --success: #50fa7b;
32
+ --code-text: #f8f8f2;
33
+ --glow-green: 0 0 8px rgba(57,255,20,0.3);
34
+ --glow-cyan: 0 0 8px rgba(0,212,255,0.3);
35
+ --glow-amber: 0 0 8px rgba(255,179,0,0.2);
36
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
37
+ --radius: 4px;
38
+ --transition: 0.2s ease;
39
+ }
40
+
41
+ html, body {
42
+ height: 100%;
43
+ background: var(--bg-deep);
44
+ color: var(--gray-light);
45
+ font-family: var(--font-mono);
46
+ font-size: 13px;
47
+ line-height: 1.6;
48
+ overflow: hidden;
49
+ }
50
+
51
+ /* CRT scanline overlay */
52
+ body::after {
53
+ content: '';
54
+ position: fixed;
55
+ inset: 0;
56
+ pointer-events: none;
57
+ z-index: 9999;
58
+ background: repeating-linear-gradient(
59
+ 0deg,
60
+ transparent,
61
+ transparent 2px,
62
+ rgba(0, 0, 0, 0.03) 2px,
63
+ rgba(0, 0, 0, 0.03) 4px
64
+ );
65
+ }
66
+
67
+ ::selection {
68
+ background: rgba(57, 255, 20, 0.25);
69
+ color: #fff;
70
+ }
71
+
72
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
73
+ ::-webkit-scrollbar-track { background: transparent; }
74
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
75
+ ::-webkit-scrollbar-thumb:hover { background: var(--gray-dim); }
76
+
77
+ a { color: var(--cyan); text-decoration: none; }
78
+ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
79
+
80
+ /* ═══════════════════════════════════════════════════════
81
+ APP SHELL
82
+ ═══════════════════════════════════════════════════════ */
83
+ #app {
84
+ display: flex;
85
+ flex-direction: column;
86
+ height: 100vh;
87
+ max-height: 100vh;
88
+ }
89
+
90
+ /* ═══════════════════════════════════════════════════════
91
+ CONFIG WARNING
92
+ ═══════════════════════════════════════════════════════ */
93
+ #config-warning {
94
+ display: none;
95
+ background: linear-gradient(90deg, rgba(255,85,85,0.1), rgba(255,179,0,0.1));
96
+ border-bottom: 1px solid var(--red);
97
+ padding: 8px 16px;
98
+ font-size: 12px;
99
+ color: var(--amber);
100
+ text-align: center;
101
+ text-shadow: var(--glow-amber);
102
+ }
103
+ #config-warning.visible { display: block; }
104
+
105
+ #playground-banner {
106
+ background: linear-gradient(90deg, rgba(0,212,255,0.08), rgba(57,255,20,0.05));
107
+ border-bottom: 1px solid var(--border);
108
+ color: var(--gray-mid);
109
+ font-size: 12px;
110
+ padding: 7px 18px;
111
+ text-align: center;
112
+ flex-shrink: 0;
113
+ }
114
+ #playground-banner strong {
115
+ color: var(--gray-light);
116
+ font-weight: 600;
117
+ }
118
+ #playground-banner a {
119
+ color: var(--cyan);
120
+ font-weight: 600;
121
+ }
122
+
123
+ /* ═══════════════════════════════════════════════════════
124
+ HEADER
125
+ ═══════════════════════════════════════════════════════ */
126
+ #header {
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: space-between;
130
+ padding: 12px 20px;
131
+ background: var(--bg-panel);
132
+ border-bottom: 1px solid var(--border);
133
+ flex-shrink: 0;
134
+ gap: 16px;
135
+ }
136
+
137
+ .header-title {
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 2px;
141
+ }
142
+
143
+ .header-ascii {
144
+ color: var(--green);
145
+ font-size: 11px;
146
+ line-height: 1.3;
147
+ text-shadow: var(--glow-green);
148
+ white-space: pre;
149
+ letter-spacing: 0.5px;
150
+ }
151
+
152
+ .header-subtitle {
153
+ color: var(--gray-mid);
154
+ font-size: 11px;
155
+ padding-left: 3px;
156
+ }
157
+
158
+ .header-actions {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 10px;
162
+ flex-shrink: 0;
163
+ }
164
+
165
+ .pill {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ gap: 5px;
169
+ padding: 4px 10px;
170
+ border: 1px solid var(--border);
171
+ border-radius: 12px;
172
+ font-size: 11px;
173
+ color: var(--gray-mid);
174
+ text-decoration: none;
175
+ transition: all var(--transition);
176
+ }
177
+ .pill:hover {
178
+ border-color: var(--cyan);
179
+ color: var(--cyan);
180
+ text-decoration: none;
181
+ text-shadow: var(--glow-cyan);
182
+ }
183
+ .pill .dot {
184
+ width: 6px;
185
+ height: 6px;
186
+ border-radius: 50%;
187
+ background: var(--success);
188
+ box-shadow: 0 0 6px var(--success);
189
+ }
190
+
191
+ #btn-new-chat {
192
+ background: transparent;
193
+ border: 1px solid var(--border);
194
+ color: var(--amber);
195
+ font-family: var(--font-mono);
196
+ font-size: 11px;
197
+ padding: 5px 12px;
198
+ border-radius: var(--radius);
199
+ cursor: pointer;
200
+ transition: all var(--transition);
201
+ letter-spacing: 1px;
202
+ }
203
+ #btn-new-chat:hover {
204
+ border-color: var(--amber);
205
+ background: rgba(255,179,0,0.08);
206
+ text-shadow: var(--glow-amber);
207
+ }
208
+
209
+ /* ═══════════════════════════════════════════════════════
210
+ MAIN LAYOUT
211
+ ═══════════════════════════════════════════════════════ */
212
+ #main {
213
+ display: flex;
214
+ flex: 1;
215
+ min-height: 0;
216
+ overflow: hidden;
217
+ }
218
+
219
+ /* ═══════════════════════════════════════════════════════
220
+ TERMINAL (LEFT PANEL)
221
+ ═══════════════════════════════════════════════════════ */
222
+ #terminal-panel {
223
+ display: flex;
224
+ flex-direction: column;
225
+ flex: 1;
226
+ min-width: 0;
227
+ border-right: 1px solid var(--border);
228
+ }
229
+
230
+ .panel-label {
231
+ padding: 6px 16px;
232
+ font-size: 10px;
233
+ letter-spacing: 2px;
234
+ color: var(--gray-dim);
235
+ border-bottom: 1px solid var(--border);
236
+ background: rgba(13,17,23,0.6);
237
+ text-transform: uppercase;
238
+ flex-shrink: 0;
239
+ }
240
+
241
+ #chat-messages {
242
+ flex: 1;
243
+ overflow-y: auto;
244
+ padding: 16px;
245
+ scroll-behavior: smooth;
246
+ }
247
+
248
+ /* Message styles */
249
+ .msg {
250
+ margin-bottom: 14px;
251
+ line-height: 1.65;
252
+ animation: msgFadeIn 0.25s ease;
253
+ }
254
+
255
+ @keyframes msgFadeIn {
256
+ from { opacity: 0; transform: translateY(6px); }
257
+ to { opacity: 1; transform: translateY(0); }
258
+ }
259
+
260
+ .msg-prefix {
261
+ font-weight: 600;
262
+ margin-right: 4px;
263
+ }
264
+
265
+ .msg-user .msg-prefix {
266
+ color: var(--green);
267
+ text-shadow: var(--glow-green);
268
+ }
269
+ .msg-user .msg-content {
270
+ color: var(--green);
271
+ text-shadow: var(--glow-green);
272
+ }
273
+
274
+ .msg-assistant .msg-prefix {
275
+ color: var(--cyan);
276
+ text-shadow: var(--glow-cyan);
277
+ }
278
+ .msg-assistant .msg-content {
279
+ color: var(--gray-light);
280
+ }
281
+
282
+ .msg-system .msg-prefix {
283
+ color: var(--amber);
284
+ text-shadow: var(--glow-amber);
285
+ }
286
+ .msg-system .msg-content {
287
+ color: var(--amber);
288
+ opacity: 0.85;
289
+ }
290
+
291
+ /* Markdown elements */
292
+ .msg-content strong { color: #fff; font-weight: 600; }
293
+ .msg-content em { font-style: italic; color: var(--gray-mid); }
294
+ .msg-content code:not(pre code) {
295
+ background: var(--bg-code);
296
+ color: var(--code-text);
297
+ padding: 1px 5px;
298
+ border-radius: 3px;
299
+ font-size: 12px;
300
+ border: 1px solid var(--border);
301
+ }
302
+ .msg-content a { color: var(--cyan); }
303
+ .msg-content ul, .msg-content ol {
304
+ margin: 6px 0 6px 20px;
305
+ }
306
+ .msg-content li { margin-bottom: 3px; }
307
+ .msg-content h1, .msg-content h2, .msg-content h3 {
308
+ color: var(--cyan);
309
+ margin: 10px 0 6px;
310
+ font-size: 14px;
311
+ text-shadow: var(--glow-cyan);
312
+ }
313
+ .msg-content h1 { font-size: 16px; }
314
+ .msg-content h2 { font-size: 15px; }
315
+ .msg-content p { margin: 4px 0; }
316
+
317
+ /* Code blocks */
318
+ .code-block-wrap {
319
+ margin: 8px 0;
320
+ border: 1px solid var(--border);
321
+ border-radius: var(--radius);
322
+ overflow: hidden;
323
+ background: var(--bg-code);
324
+ }
325
+ .code-block-header {
326
+ display: flex;
327
+ align-items: center;
328
+ justify-content: space-between;
329
+ padding: 4px 10px;
330
+ background: rgba(30,42,58,0.5);
331
+ border-bottom: 1px solid var(--border);
332
+ font-size: 11px;
333
+ }
334
+ .code-lang {
335
+ color: var(--amber);
336
+ text-transform: uppercase;
337
+ letter-spacing: 1px;
338
+ }
339
+ .btn-copy {
340
+ background: transparent;
341
+ border: 1px solid var(--border);
342
+ color: var(--gray-mid);
343
+ font-family: var(--font-mono);
344
+ font-size: 10px;
345
+ padding: 2px 8px;
346
+ border-radius: 3px;
347
+ cursor: pointer;
348
+ transition: all var(--transition);
349
+ }
350
+ .btn-copy:hover {
351
+ border-color: var(--green);
352
+ color: var(--green);
353
+ }
354
+ .btn-copy.copied {
355
+ border-color: var(--success);
356
+ color: var(--success);
357
+ }
358
+ .code-block-wrap pre {
359
+ margin: 0;
360
+ padding: 10px 12px;
361
+ overflow-x: auto;
362
+ font-size: 12px;
363
+ line-height: 1.5;
364
+ color: var(--code-text);
365
+ }
366
+ .code-block-wrap pre code {
367
+ font-family: var(--font-mono);
368
+ background: none;
369
+ border: none;
370
+ padding: 0;
371
+ }
372
+
373
+ /* Thinking blocks */
374
+ .think-block {
375
+ margin: 8px 0;
376
+ border: 1px solid rgba(255,179,0,0.15);
377
+ border-radius: var(--radius);
378
+ background: rgba(255,179,0,0.03);
379
+ }
380
+ .think-summary {
381
+ display: block;
382
+ width: 100%;
383
+ background: transparent;
384
+ border: none;
385
+ padding: 6px 10px;
386
+ cursor: pointer;
387
+ font-size: 12px;
388
+ font-family: var(--font-mono);
389
+ text-align: left;
390
+ color: var(--gray-dim);
391
+ user-select: none;
392
+ transition: color var(--transition);
393
+ }
394
+ .think-summary:hover { color: var(--amber); }
395
+ .think-block .think-content {
396
+ padding: 6px 12px 10px;
397
+ font-size: 12px;
398
+ color: var(--gray-dim);
399
+ line-height: 1.55;
400
+ border-top: 1px solid rgba(255,179,0,0.1);
401
+ }
402
+ .think-block:not(.open) .think-content {
403
+ display: none;
404
+ }
405
+
406
+ /* Streaming cursor */
407
+ .streaming-cursor::after {
408
+ content: '█';
409
+ animation: blink 0.8s step-end infinite;
410
+ color: var(--green);
411
+ margin-left: 2px;
412
+ }
413
+ @keyframes blink {
414
+ 50% { opacity: 0; }
415
+ }
416
+
417
+ /* ═══════════════════════════════════════════════════════
418
+ INPUT AREA
419
+ ═══════════════════════════════════════════════════════ */
420
+ #input-area {
421
+ flex-shrink: 0;
422
+ border-top: 1px solid var(--border);
423
+ background: var(--bg-panel);
424
+ padding: 10px 16px 8px;
425
+ }
426
+
427
+ /* Target toggle */
428
+ #target-toggle {
429
+ display: flex;
430
+ gap: 14px;
431
+ margin-bottom: 8px;
432
+ align-items: center;
433
+ }
434
+
435
+ .target-label {
436
+ font-size: 10px;
437
+ color: var(--gray-dim);
438
+ letter-spacing: 1px;
439
+ text-transform: uppercase;
440
+ }
441
+
442
+ .target-btn {
443
+ background: transparent;
444
+ border: none;
445
+ font-family: var(--font-mono);
446
+ font-size: 12px;
447
+ color: var(--gray-dim);
448
+ cursor: pointer;
449
+ padding: 2px 0;
450
+ border-bottom: 2px solid transparent;
451
+ transition: all var(--transition);
452
+ letter-spacing: 0.5px;
453
+ }
454
+ .target-btn:hover { color: var(--gray-mid); }
455
+ .target-btn.active {
456
+ color: var(--green);
457
+ border-bottom-color: var(--green);
458
+ text-shadow: var(--glow-green);
459
+ }
460
+
461
+ #input-row {
462
+ display: flex;
463
+ gap: 8px;
464
+ align-items: flex-end;
465
+ }
466
+
467
+ .input-prompt-symbol {
468
+ color: var(--green);
469
+ font-weight: 700;
470
+ font-size: 14px;
471
+ line-height: 36px;
472
+ text-shadow: var(--glow-green);
473
+ flex-shrink: 0;
474
+ }
475
+
476
+ #chat-input {
477
+ flex: 1;
478
+ background: var(--bg-deep);
479
+ border: 1px solid var(--border);
480
+ border-radius: var(--radius);
481
+ color: var(--green);
482
+ font-family: var(--font-mono);
483
+ font-size: 13px;
484
+ padding: 8px 12px;
485
+ resize: none;
486
+ outline: none;
487
+ min-height: 36px;
488
+ max-height: 120px;
489
+ line-height: 1.5;
490
+ transition: border-color var(--transition);
491
+ caret-color: var(--green);
492
+ text-shadow: var(--glow-green);
493
+ }
494
+ #chat-input::placeholder {
495
+ color: var(--gray-dim);
496
+ text-shadow: none;
497
+ }
498
+ #chat-input:focus {
499
+ border-color: var(--border-focus);
500
+ }
501
+
502
+ #btn-send, #btn-stop {
503
+ font-family: var(--font-mono);
504
+ font-size: 12px;
505
+ padding: 8px 14px;
506
+ border-radius: var(--radius);
507
+ cursor: pointer;
508
+ transition: all var(--transition);
509
+ letter-spacing: 1px;
510
+ flex-shrink: 0;
511
+ height: 36px;
512
+ display: flex;
513
+ align-items: center;
514
+ gap: 4px;
515
+ }
516
+
517
+ #btn-send {
518
+ background: transparent;
519
+ border: 1px solid var(--green);
520
+ color: var(--green);
521
+ }
522
+ #btn-send:hover:not(:disabled) {
523
+ background: var(--green);
524
+ color: var(--bg-deep);
525
+ box-shadow: 0 0 12px rgba(57,255,20,0.3);
526
+ }
527
+ #btn-send:disabled {
528
+ opacity: 0.3;
529
+ cursor: not-allowed;
530
+ }
531
+
532
+ #btn-stop {
533
+ background: transparent;
534
+ border: 1px solid var(--red);
535
+ color: var(--red);
536
+ display: none;
537
+ }
538
+ #btn-stop:hover {
539
+ background: var(--red);
540
+ color: var(--bg-deep);
541
+ box-shadow: 0 0 12px rgba(255,85,85,0.3);
542
+ }
543
+
544
+ /* Examples */
545
+ #examples-row {
546
+ display: flex;
547
+ align-items: center;
548
+ gap: 8px;
549
+ margin-top: 8px;
550
+ flex-wrap: wrap;
551
+ }
552
+ .examples-label {
553
+ font-size: 10px;
554
+ color: var(--gray-dim);
555
+ letter-spacing: 1px;
556
+ text-transform: uppercase;
557
+ flex-shrink: 0;
558
+ }
559
+ .example-chip {
560
+ background: rgba(30,42,58,0.4);
561
+ border: 1px solid var(--border);
562
+ border-radius: 12px;
563
+ padding: 3px 10px;
564
+ font-family: var(--font-mono);
565
+ font-size: 11px;
566
+ color: var(--gray-mid);
567
+ cursor: pointer;
568
+ transition: all var(--transition);
569
+ white-space: nowrap;
570
+ }
571
+ .example-chip:hover {
572
+ border-color: var(--cyan);
573
+ color: var(--cyan);
574
+ background: rgba(0,212,255,0.05);
575
+ text-shadow: var(--glow-cyan);
576
+ }
577
+
578
+ /* ═══════════════════════════════════════════════════════
579
+ OUTPUT PANEL (RIGHT)
580
+ ═══════════════════════════════════════════════════════ */
581
+ #output-panel {
582
+ display: flex;
583
+ flex-direction: column;
584
+ width: 45%;
585
+ min-width: 340px;
586
+ max-width: 55%;
587
+ min-height: 0;
588
+ background: var(--bg-panel);
589
+ }
590
+
591
+ #output-tabs {
592
+ display: flex;
593
+ border-bottom: 1px solid var(--border);
594
+ background: rgba(13,17,23,0.6);
595
+ flex-shrink: 0;
596
+ }
597
+
598
+ .output-tab {
599
+ flex: 1;
600
+ background: transparent;
601
+ border: none;
602
+ border-bottom: 2px solid transparent;
603
+ color: var(--gray-dim);
604
+ font-family: var(--font-mono);
605
+ font-size: 11px;
606
+ padding: 8px 12px;
607
+ cursor: pointer;
608
+ transition: all var(--transition);
609
+ letter-spacing: 1px;
610
+ text-transform: uppercase;
611
+ }
612
+ .output-tab:hover { color: var(--gray-mid); }
613
+ .output-tab.active {
614
+ color: var(--cyan);
615
+ border-bottom-color: var(--cyan);
616
+ text-shadow: var(--glow-cyan);
617
+ }
618
+
619
+ #output-content {
620
+ flex: 1;
621
+ min-height: 0;
622
+ overflow: hidden;
623
+ position: relative;
624
+ }
625
+
626
+ /* Tab panes */
627
+ .tab-pane { display: none; height: 100%; min-height: 0; }
628
+ .tab-pane.active { display: flex; flex-direction: column; }
629
+
630
+ /* Preview tab */
631
+ #pane-preview {
632
+ align-items: stretch;
633
+ justify-content: stretch;
634
+ position: relative;
635
+ min-height: 0;
636
+ overflow: hidden;
637
+ }
638
+
639
+ .preview-placeholder {
640
+ align-self: center;
641
+ margin: auto;
642
+ text-align: center;
643
+ color: var(--gray-dim);
644
+ padding: 40px 20px;
645
+ }
646
+ .preview-placeholder .ascii-art {
647
+ font-size: 11px;
648
+ line-height: 1.3;
649
+ margin-bottom: 16px;
650
+ color: var(--border-focus);
651
+ }
652
+ .preview-placeholder .placeholder-text {
653
+ font-size: 12px;
654
+ letter-spacing: 0.5px;
655
+ }
656
+
657
+ #preview-image {
658
+ display: none;
659
+ max-width: 100%;
660
+ max-height: 100%;
661
+ object-fit: contain;
662
+ padding: 12px;
663
+ }
664
+
665
+ #preview-iframe {
666
+ display: none;
667
+ position: absolute;
668
+ inset: 0;
669
+ width: 100%;
670
+ height: 100%;
671
+ min-height: 0;
672
+ border: none;
673
+ background: #fff;
674
+ }
675
+
676
+ #btn-fullscreen {
677
+ display: none;
678
+ position: absolute;
679
+ top: 8px;
680
+ right: 8px;
681
+ background: rgba(13,17,23,0.8);
682
+ border: 1px solid var(--border);
683
+ color: var(--gray-mid);
684
+ font-family: var(--font-mono);
685
+ font-size: 11px;
686
+ padding: 4px 10px;
687
+ border-radius: var(--radius);
688
+ cursor: pointer;
689
+ z-index: 5;
690
+ transition: all var(--transition);
691
+ }
692
+ #btn-fullscreen:hover {
693
+ border-color: var(--cyan);
694
+ color: var(--cyan);
695
+ }
696
+
697
+ /* Console tab */
698
+ #pane-console { padding: 12px 16px; gap: 12px; overflow-y: auto; }
699
+
700
+ .console-section { margin-bottom: 8px; }
701
+ .console-label {
702
+ font-size: 10px;
703
+ letter-spacing: 2px;
704
+ color: var(--gray-dim);
705
+ margin-bottom: 4px;
706
+ text-transform: uppercase;
707
+ }
708
+ .console-output {
709
+ background: var(--bg-deep);
710
+ border: 1px solid var(--border);
711
+ border-radius: var(--radius);
712
+ padding: 10px 12px;
713
+ font-size: 12px;
714
+ line-height: 1.5;
715
+ white-space: pre-wrap;
716
+ word-break: break-word;
717
+ min-height: 40px;
718
+ max-height: 280px;
719
+ overflow-y: auto;
720
+ }
721
+ #console-stdout { color: var(--success); }
722
+ #console-stderr { color: var(--red); }
723
+
724
+ /* Code tab */
725
+ #pane-code { padding: 0; }
726
+
727
+ .code-tab-header {
728
+ display: flex;
729
+ align-items: center;
730
+ justify-content: space-between;
731
+ padding: 8px 12px;
732
+ border-bottom: 1px solid var(--border);
733
+ background: rgba(30,42,58,0.3);
734
+ flex-shrink: 0;
735
+ }
736
+ .code-tab-lang {
737
+ font-size: 11px;
738
+ color: var(--amber);
739
+ letter-spacing: 1px;
740
+ text-transform: uppercase;
741
+ }
742
+ .code-tab-actions {
743
+ display: flex;
744
+ gap: 8px;
745
+ }
746
+ .code-tab-btn {
747
+ background: transparent;
748
+ border: 1px solid var(--border);
749
+ color: var(--gray-mid);
750
+ font-family: var(--font-mono);
751
+ font-size: 10px;
752
+ padding: 3px 8px;
753
+ border-radius: 3px;
754
+ cursor: pointer;
755
+ text-decoration: none;
756
+ transition: all var(--transition);
757
+ display: inline-flex;
758
+ align-items: center;
759
+ gap: 4px;
760
+ }
761
+ .code-tab-btn:hover {
762
+ border-color: var(--cyan);
763
+ color: var(--cyan);
764
+ text-decoration: none;
765
+ }
766
+
767
+ #code-display {
768
+ flex: 1;
769
+ overflow: auto;
770
+ padding: 12px;
771
+ background: var(--bg-code);
772
+ }
773
+ #code-display pre {
774
+ margin: 0;
775
+ font-size: 12px;
776
+ line-height: 1.5;
777
+ color: var(--code-text);
778
+ }
779
+
780
+ .code-placeholder {
781
+ display: flex;
782
+ align-items: center;
783
+ justify-content: center;
784
+ height: 100%;
785
+ color: var(--gray-dim);
786
+ font-size: 12px;
787
+ }
788
+
789
+ /* ═══════════════════════════════════════════════════════
790
+ STATUS BAR
791
+ ═══════════════════════════════════════════════════════ */
792
+ #status-bar {
793
+ display: flex;
794
+ align-items: center;
795
+ gap: 8px;
796
+ padding: 5px 16px;
797
+ border-top: 1px solid var(--border);
798
+ background: var(--bg-panel);
799
+ font-size: 11px;
800
+ flex-shrink: 0;
801
+ }
802
+
803
+ .status-indicator {
804
+ display: inline-flex;
805
+ align-items: center;
806
+ gap: 6px;
807
+ }
808
+
809
+ .status-dot {
810
+ font-size: 10px;
811
+ line-height: 1;
812
+ }
813
+
814
+ #status-text {
815
+ letter-spacing: 1px;
816
+ text-transform: uppercase;
817
+ }
818
+
819
+ .status-idle { color: var(--gray-dim); }
820
+ .status-working { color: var(--amber); text-shadow: var(--glow-amber); }
821
+ .status-success { color: var(--success); text-shadow: 0 0 8px rgba(80,250,123,0.3); }
822
+ .status-error { color: var(--red); text-shadow: 0 0 8px rgba(255,85,85,0.3); }
823
+ .status-info { color: var(--cyan); text-shadow: var(--glow-cyan); }
824
+
825
+ @keyframes spin {
826
+ to { transform: rotate(360deg); }
827
+ }
828
+ .status-working .status-dot {
829
+ display: inline-block;
830
+ animation: spin 1s linear infinite;
831
+ }
832
+
833
+ /* ═══════════════════════════════════════════════════════
834
+ FULLSCREEN OVERLAY
835
+ ═══════════════════════════════════════════════════════ */
836
+ #fullscreen-overlay {
837
+ display: none;
838
+ position: fixed;
839
+ inset: 0;
840
+ z-index: 1000;
841
+ background: var(--bg-deep);
842
+ flex-direction: column;
843
+ }
844
+ #fullscreen-overlay.active { display: flex; }
845
+
846
+ #fullscreen-bar {
847
+ display: flex;
848
+ align-items: center;
849
+ justify-content: space-between;
850
+ padding: 8px 16px;
851
+ border-bottom: 1px solid var(--border);
852
+ background: var(--bg-panel);
853
+ }
854
+ #fullscreen-bar span {
855
+ color: var(--cyan);
856
+ font-size: 12px;
857
+ letter-spacing: 1px;
858
+ }
859
+ #btn-exit-fullscreen {
860
+ background: transparent;
861
+ border: 1px solid var(--border);
862
+ color: var(--gray-mid);
863
+ font-family: var(--font-mono);
864
+ font-size: 11px;
865
+ padding: 4px 12px;
866
+ border-radius: var(--radius);
867
+ cursor: pointer;
868
+ transition: all var(--transition);
869
+ }
870
+ #btn-exit-fullscreen:hover {
871
+ border-color: var(--red);
872
+ color: var(--red);
873
+ }
874
+ #fullscreen-iframe {
875
+ flex: 1;
876
+ border: none;
877
+ background: #fff;
878
+ }
879
+
880
+ /* ═══════════════════════════════════════════════════════
881
+ RESPONSIVE
882
+ ═══════════════════════════════════════════════════════ */
883
+ @media (max-width: 900px) {
884
+ #main {
885
+ flex-direction: column;
886
+ }
887
+ #terminal-panel {
888
+ border-right: none;
889
+ border-bottom: 1px solid var(--border);
890
+ max-height: 55vh;
891
+ }
892
+ #output-panel {
893
+ width: 100%;
894
+ max-width: 100%;
895
+ min-width: 0;
896
+ flex: 1;
897
+ }
898
+ .header-ascii { font-size: 10px; }
899
+ #chat-input { font-size: 12px; }
900
+ #preview-iframe { min-height: 400px; }
901
+ }
902
+
903
+ @media (max-width: 600px) {
904
+ #header { padding: 8px 12px; gap: 8px; }
905
+ .header-ascii { display: none; }
906
+ .header-subtitle { display: none; }
907
+ .pill { font-size: 10px; padding: 3px 8px; }
908
+ #chat-messages { padding: 10px; }
909
+ #input-area { padding: 8px 10px 6px; }
910
+ #examples-row { display: none; }
911
+ }
912
+ </style>
913
+ </head>
914
+ <body>
915
+ <div id="app">
916
+ <!-- Config Warning -->
917
+ <div id="config-warning">⚠ WARNING: COHERE_API_KEY not configured. Set it as a Space secret to enable the model.</div>
918
+
919
+ <!-- Header -->
920
+ <header id="header">
921
+ <div class="header-title">
922
+ <div class="header-ascii" id="header-ascii-art">╔═══ NORTH MINI CODE 1.0 ═══╗</div>
923
+ <div class="header-subtitle" id="header-subtitle">Terminal Coding Assistant</div>
924
+ </div>
925
+ <div class="header-actions">
926
+ <a class="pill" id="model-pill" href="#" target="_blank" rel="noopener">
927
+ <span class="dot"></span>
928
+ <span id="model-pill-text">north-mini-code-1-0</span>
929
+ </a>
930
+ <a class="pill" id="opencode-pill" href="#" target="_blank" rel="noopener">OpenCode</a>
931
+ <button id="btn-new-chat" onclick="newChat()" title="Start a new chat session">[NEW]</button>
932
+ </div>
933
+ </header>
934
+
935
+ <div id="playground-banner">
936
+ <a id="banner-model-link" href="https://huggingface.co/CohereLabs/North-Mini-Code-1.0" target="_blank" rel="noopener"><strong>North-Mini-Code-1.0</strong></a>
937
+ is built for agentic coding and works best in your terminal with
938
+ <a id="opencode-banner-link" href="#" target="_blank" rel="noopener">OpenCode ↗</a>.
939
+ This Space is a browser playground for trying the model.
940
+ </div>
941
+
942
+ <!-- Main Layout -->
943
+ <div id="main">
944
+ <!-- Terminal Panel -->
945
+ <div id="terminal-panel">
946
+ <div class="panel-label">Terminal</div>
947
+ <div id="chat-messages"></div>
948
+ <div id="input-area">
949
+ <div id="target-toggle">
950
+ <span class="target-label">Mode:</span>
951
+ <button class="target-btn active" data-target="Python" onclick="setTarget('Python')">[ Python ● ]</button>
952
+ <button class="target-btn" data-target="Web" onclick="setTarget('Web')">[ Web ○ ]</button>
953
+ </div>
954
+ <div id="input-row">
955
+ <span class="input-prompt-symbol">❯</span>
956
+ <textarea id="chat-input" rows="1" placeholder="Ask a coding question or describe what to build…" spellcheck="false"></textarea>
957
+ <button id="btn-send" onclick="handleSend()" title="Send message (Shift+Enter)">➤</button>
958
+ <button id="btn-stop" onclick="stopGeneration()" title="Stop generation">■ STOP</button>
959
+ </div>
960
+ <div id="examples-row"></div>
961
+ </div>
962
+ </div>
963
+
964
+ <!-- Output Panel -->
965
+ <div id="output-panel">
966
+ <div id="output-tabs">
967
+ <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button>
968
+ <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
969
+ <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
970
+ </div>
971
+ <div id="output-content">
972
+ <!-- Preview Pane -->
973
+ <div class="tab-pane active" id="pane-preview">
974
+ <div class="preview-placeholder" id="preview-placeholder">
975
+ <div class="ascii-art">
976
+ ┌──────────────────────┐
977
+ │ ╭━━━╮ │
978
+ │ ┃ ▶ ┃ OUTPUT │
979
+ │ ╰━━━╯ │
980
+ └──────────────────────┘</div>
981
+ <div class="placeholder-text">Run code to see output here</div>
982
+ </div>
983
+ <img id="preview-image" alt="Generated output">
984
+ <iframe id="preview-iframe" sandbox="allow-scripts"></iframe>
985
+ <button id="btn-fullscreen" onclick="openFullscreen()">⤢ FULLSCREEN</button>
986
+ </div>
987
+
988
+ <!-- Console Pane -->
989
+ <div class="tab-pane" id="pane-console">
990
+ <div class="console-section">
991
+ <div class="console-label">stdout:</div>
992
+ <div class="console-output" id="console-stdout">No output yet.</div>
993
+ </div>
994
+ <div class="console-section">
995
+ <div class="console-label">stderr:</div>
996
+ <div class="console-output" id="console-stderr">No errors.</div>
997
+ </div>
998
+ </div>
999
+
1000
+ <!-- Code Pane -->
1001
+ <div class="tab-pane" id="pane-code">
1002
+ <div class="code-tab-header">
1003
+ <span class="code-tab-lang" id="code-tab-lang">—</span>
1004
+ <div class="code-tab-actions">
1005
+ <button class="code-tab-btn" id="btn-copy-code" onclick="copyCode()">📋 Copy</button>
1006
+ <a class="code-tab-btn" id="btn-download" href="#" style="display:none;">⬇ Download</a>
1007
+ </div>
1008
+ </div>
1009
+ <div id="code-display">
1010
+ <div class="code-placeholder">No code generated yet.</div>
1011
+ </div>
1012
+ </div>
1013
+ </div>
1014
+ </div>
1015
+ </div>
1016
+
1017
+ <!-- Status Bar -->
1018
+ <div id="status-bar">
1019
+ <div class="status-indicator status-idle" id="status-indicator">
1020
+ <span class="status-dot">●</span>
1021
+ <span id="status-text">IDLE</span>
1022
+ </div>
1023
+ </div>
1024
+ </div>
1025
+
1026
+ <!-- Fullscreen Overlay -->
1027
+ <div id="fullscreen-overlay">
1028
+ <div id="fullscreen-bar">
1029
+ <span>WEB PREVIEW</span>
1030
+ <button id="btn-exit-fullscreen" onclick="closeFullscreen()">[✕ CLOSE]</button>
1031
+ </div>
1032
+ <iframe id="fullscreen-iframe" sandbox="allow-scripts"></iframe>
1033
+ </div>
1034
+
1035
+ <script>
1036
+ // ═══════════════════════════════════════════════════════
1037
+ // CONFIG
1038
+ // ═══════════════════════════════════════════════════════
1039
+ const CONFIG = __RUNTIME_CONFIG__;
1040
+
1041
+ // ═══════════════════════════════════════════════════════
1042
+ // STATE
1043
+ // ═══════════════════════════════════════════════════════
1044
+ const state = {
1045
+ history: [],
1046
+ executionContext: {},
1047
+ targetLanguage: 'Python',
1048
+ isGenerating: false,
1049
+ currentEventSource: null,
1050
+ activeTab: 'preview',
1051
+ lastExecution: null,
1052
+ lastCode: '',
1053
+ lastCodeLang: '',
1054
+ pendingWebPreviewCode: '',
1055
+ loadedWebPreviewCode: '',
1056
+ scheduledWebPreviewCode: '',
1057
+ reasoningExpanded: false,
1058
+ lastReasoningPressAt: 0
1059
+ };
1060
+
1061
+ // ═══════════════════════════════════════════════════════
1062
+ // INITIALIZATION
1063
+ // ═══════════════════════════════════════════════════════
1064
+ document.addEventListener('DOMContentLoaded', () => {
1065
+ // Apply config
1066
+ document.title = CONFIG.app_title || 'North Mini Code 1.0';
1067
+
1068
+ if (CONFIG.model_url) {
1069
+ document.getElementById('model-pill').href = CONFIG.model_url;
1070
+ document.getElementById('banner-model-link').href = CONFIG.model_url;
1071
+ }
1072
+ if (CONFIG.model_id) {
1073
+ document.getElementById('model-pill-text').textContent = CONFIG.model_id;
1074
+ }
1075
+ if (CONFIG.opencode_url) {
1076
+ document.getElementById('opencode-pill').href = CONFIG.opencode_url;
1077
+ document.getElementById('opencode-banner-link').href = CONFIG.opencode_url;
1078
+ }
1079
+
1080
+ // Config warning
1081
+ if (!CONFIG.api_key_configured) {
1082
+ document.getElementById('config-warning').classList.add('visible');
1083
+ }
1084
+
1085
+ // Render examples
1086
+ renderExamples();
1087
+
1088
+ // Welcome message
1089
+ addSystemMessage(`Welcome to ${CONFIG.app_title || 'North Mini Code 1.0'}. Type a coding question or select an example below to get started.`);
1090
+
1091
+ // Input auto-resize & keybinding
1092
+ const input = document.getElementById('chat-input');
1093
+ input.addEventListener('input', autoResize);
1094
+ input.addEventListener('keydown', (e) => {
1095
+ if (e.key === 'Enter' && e.shiftKey) {
1096
+ e.preventDefault();
1097
+ handleSend();
1098
+ }
1099
+ });
1100
+
1101
+ document.addEventListener('pointerdown', handleReasoningPress, true);
1102
+ document.addEventListener('mousedown', handleReasoningPress, true);
1103
+ document.addEventListener('keydown', handleReasoningKeydown, true);
1104
+ document.addEventListener('keydown', handleFullscreenKeydown);
1105
+ observePreviewSize();
1106
+ });
1107
+
1108
+ function autoResize() {
1109
+ const el = document.getElementById('chat-input');
1110
+ el.style.height = 'auto';
1111
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
1112
+ }
1113
+
1114
+ // ═══════════════════════════════════════════════════════
1115
+ // EXAMPLES
1116
+ // ═══════════════════════════════════════════════════════
1117
+ function renderExamples() {
1118
+ const row = document.getElementById('examples-row');
1119
+ if (!CONFIG.examples || CONFIG.examples.length === 0) {
1120
+ row.style.display = 'none';
1121
+ return;
1122
+ }
1123
+ row.innerHTML = '<span class="examples-label">Try:</span>';
1124
+ CONFIG.examples.forEach((ex) => {
1125
+ const chip = document.createElement('button');
1126
+ chip.className = 'example-chip';
1127
+ chip.textContent = ex.label;
1128
+ chip.title = ex.prompt;
1129
+ chip.addEventListener('click', () => {
1130
+ if (state.isGenerating) return;
1131
+ resetConversation();
1132
+ if (ex.target) setTarget(ex.target);
1133
+ sendMessage(ex.prompt);
1134
+ });
1135
+ row.appendChild(chip);
1136
+ });
1137
+ }
1138
+
1139
+ // ═══════════════════════════════════════════════════════
1140
+ // TARGET LANGUAGE
1141
+ // ═══════════════════════════════════════════════════════
1142
+ function setTarget(target) {
1143
+ state.targetLanguage = target;
1144
+ document.querySelectorAll('.target-btn').forEach((btn) => {
1145
+ const isActive = btn.dataset.target === target;
1146
+ btn.classList.toggle('active', isActive);
1147
+ btn.textContent = `[ ${btn.dataset.target} ${isActive ? '●' : '○'} ]`;
1148
+ });
1149
+ }
1150
+
1151
+ // ═══════════════════════════════════════════════════════
1152
+ // CHAT MESSAGES
1153
+ // ═══════════════════════════════════════════════════════
1154
+ function addSystemMessage(text) {
1155
+ const container = document.getElementById('chat-messages');
1156
+ const div = document.createElement('div');
1157
+ div.className = 'msg msg-system';
1158
+ div.innerHTML = `<span class="msg-prefix">system&gt;</span><span class="msg-content">${escapeHtml(text)}</span>`;
1159
+ container.appendChild(div);
1160
+ scrollToBottom();
1161
+ }
1162
+
1163
+ function addUserMessage(text) {
1164
+ const container = document.getElementById('chat-messages');
1165
+ const div = document.createElement('div');
1166
+ div.className = 'msg msg-user';
1167
+ div.innerHTML = `<span class="msg-prefix">user&gt;</span><span class="msg-content">${escapeHtml(text)}</span>`;
1168
+ container.appendChild(div);
1169
+ scrollToBottom();
1170
+ }
1171
+
1172
+ function addAssistantMessage() {
1173
+ const container = document.getElementById('chat-messages');
1174
+ const div = document.createElement('div');
1175
+ div.className = 'msg msg-assistant';
1176
+ div.id = 'current-assistant-msg';
1177
+ div.innerHTML = `<span class="msg-prefix">north&gt;</span><span class="msg-content streaming-cursor"></span>`;
1178
+ container.appendChild(div);
1179
+ state.reasoningExpanded = false;
1180
+ scrollToBottom();
1181
+ return div;
1182
+ }
1183
+
1184
+ function updateAssistantMessage(content, isStreaming) {
1185
+ const div = document.getElementById('current-assistant-msg');
1186
+ if (!div) return;
1187
+ const contentEl = div.querySelector('.msg-content');
1188
+ const keepReasoningExpanded = state.reasoningExpanded || Boolean(contentEl.querySelector('.think-block.open'));
1189
+ state.reasoningExpanded = keepReasoningExpanded;
1190
+ contentEl.innerHTML = parseMarkdown(content);
1191
+ contentEl.querySelectorAll('.think-block').forEach((block) => {
1192
+ setReasoningBlockOpen(block, keepReasoningExpanded);
1193
+ });
1194
+ if (isStreaming) {
1195
+ contentEl.classList.add('streaming-cursor');
1196
+ } else {
1197
+ contentEl.classList.remove('streaming-cursor');
1198
+ }
1199
+ scrollToBottom();
1200
+ }
1201
+
1202
+ function finalizeAssistantMessage() {
1203
+ const div = document.getElementById('current-assistant-msg');
1204
+ if (div) {
1205
+ div.id = '';
1206
+ const contentEl = div.querySelector('.msg-content');
1207
+ if (contentEl) contentEl.classList.remove('streaming-cursor');
1208
+ }
1209
+ }
1210
+
1211
+ function scrollToBottom() {
1212
+ const container = document.getElementById('chat-messages');
1213
+ requestAnimationFrame(() => {
1214
+ container.scrollTop = container.scrollHeight;
1215
+ });
1216
+ }
1217
+
1218
+ // ═══════════════════════════════════════════════════════
1219
+ // MARKDOWN PARSER
1220
+ // ═══════════════════════════════════════════════════════
1221
+ function parseMarkdown(text) {
1222
+ if (!text) return '';
1223
+
1224
+ // Extract reasoning blocks before escaping so they render as collapsed details.
1225
+ const thinkBlocks = [];
1226
+ text = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, content) => {
1227
+ const idx = thinkBlocks.length;
1228
+ thinkBlocks.push(renderThinkBlock(content, '💭 Reasoning (click to expand)'));
1229
+ return `@@THINKBLOCK_${idx}@@`;
1230
+ });
1231
+ // Handle unclosed think blocks (during streaming)
1232
+ text = text.replace(/<think>([\s\S]*)$/g, (_, content) => {
1233
+ const idx = thinkBlocks.length;
1234
+ thinkBlocks.push(renderThinkBlock(content, '💭 Reasoning (thinking…)'));
1235
+ return `@@THINKBLOCK_${idx}@@`;
1236
+ });
1237
+
1238
+ // Extract code blocks first to prevent inner processing
1239
+ const codeBlocks = [];
1240
+ text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
1241
+ const idx = codeBlocks.length;
1242
+ codeBlocks.push({ lang: lang || 'text', code: code.trimEnd() });
1243
+ return `@@CODEBLOCK_${idx}@@`;
1244
+ });
1245
+
1246
+ // Escape HTML in remaining text
1247
+ text = escapeHtml(text);
1248
+
1249
+ // Inline formatting
1250
+ text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1251
+ text = text.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, '<em>$1</em>');
1252
+ text = text.replace(/`([^`]+?)`/g, '<code>$1</code>');
1253
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
1254
+
1255
+ // Headings
1256
+ text = text.replace(/^### (.+)$/gm, '<h3>$1</h3>');
1257
+ text = text.replace(/^## (.+)$/gm, '<h2>$1</h2>');
1258
+ text = text.replace(/^# (.+)$/gm, '<h1>$1</h1>');
1259
+
1260
+ // Lists - simple approach
1261
+ // Unordered
1262
+ text = text.replace(/^(?:[-*]) (.+)$/gm, '<li>$1</li>');
1263
+ text = text.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
1264
+ // Ordered
1265
+ text = text.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
1266
+ // Wrap consecutive <li> that aren't in <ul> into <ol>
1267
+ text = text.replace(/(<li>(?:(?!<\/?[uo]l>).)*<\/li>(?:\s*<li>(?:(?!<\/?[uo]l>).)*<\/li>)*)/g, (match) => {
1268
+ if (!match.includes('<ul>') && !match.includes('</ul>')) {
1269
+ return '<ol>' + match + '</ol>';
1270
+ }
1271
+ return match;
1272
+ });
1273
+
1274
+ // Restore block-level placeholders after inline parsing so their contents remain literal.
1275
+ text = text.replace(/@@CODEBLOCK_(\d+)@@/g, (_, idx) => {
1276
+ const block = codeBlocks[parseInt(idx)];
1277
+ const escapedCode = escapeHtml(block.code);
1278
+ const id = `code-${Date.now()}-${idx}`;
1279
+ return `<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">${escapeHtml(block.lang)}</span><button class="btn-copy" onclick="copyBlock(this, '${id}')">📋 Copy</button></div><pre><code id="${id}">${escapedCode}</code></pre></div>`;
1280
+ });
1281
+ text = text.replace(/@@THINKBLOCK_(\d+)@@/g, (_, idx) => thinkBlocks[parseInt(idx)]);
1282
+
1283
+ // Line breaks (preserve paragraph structure)
1284
+ text = text.replace(/\n\n/g, '</p><p>');
1285
+ text = text.replace(/\n/g, '<br>');
1286
+ text = '<p>' + text + '</p>';
1287
+
1288
+ // Clean up empty paragraphs
1289
+ text = text.replace(/<p>\s*<\/p>/g, '');
1290
+ text = text.replace(/<p>(<(?:div|ul|ol|h[1-3]))/g, '$1');
1291
+ text = text.replace(/(<\/(?:div|ul|ol|h[1-3])>)<\/p>/g, '$1');
1292
+
1293
+ return text;
1294
+ }
1295
+
1296
+ function renderThinkBlock(content, summary) {
1297
+ const escapedContent = escapeHtml(content.trim()).replace(/\n/g, '<br>');
1298
+ const openClass = state.reasoningExpanded ? ' open' : '';
1299
+ const expanded = state.reasoningExpanded ? 'true' : 'false';
1300
+ return `<div class="think-block${openClass}"><button type="button" class="think-summary" aria-expanded="${expanded}">${summary}</button><div class="think-content">${escapedContent}</div></div>`;
1301
+ }
1302
+
1303
+ function handleReasoningPress(event) {
1304
+ updateReasoningFromEvent(event);
1305
+ }
1306
+
1307
+ function handleReasoningKeydown(event) {
1308
+ if (event.key !== 'Enter' && event.key !== ' ') return;
1309
+ updateReasoningFromEvent(event);
1310
+ }
1311
+
1312
+ function updateReasoningFromEvent(event) {
1313
+ if (event.type === 'mousedown' && Date.now() - state.lastReasoningPressAt < 500) {
1314
+ return;
1315
+ }
1316
+ const target = event.target;
1317
+ if (!target || !target.closest) return;
1318
+ const button = target.closest('.think-summary');
1319
+ if (!button) return;
1320
+ const block = button.closest('.think-block');
1321
+ if (!block) return;
1322
+ event.preventDefault();
1323
+ event.stopPropagation();
1324
+ if (event.stopImmediatePropagation) {
1325
+ event.stopImmediatePropagation();
1326
+ }
1327
+ state.lastReasoningPressAt = Date.now();
1328
+ const nextOpen = !block.classList.contains('open');
1329
+ state.reasoningExpanded = nextOpen;
1330
+ const scope = block.closest('.msg-content') || document;
1331
+ scope.querySelectorAll('.think-block').forEach((trace) => {
1332
+ setReasoningBlockOpen(trace, nextOpen);
1333
+ });
1334
+ }
1335
+
1336
+ function setReasoningBlockOpen(block, open) {
1337
+ block.classList.toggle('open', open);
1338
+ const button = block.querySelector('.think-summary');
1339
+ if (button) {
1340
+ button.setAttribute('aria-expanded', open ? 'true' : 'false');
1341
+ }
1342
+ }
1343
+
1344
+ function escapeHtml(text) {
1345
+ const div = document.createElement('div');
1346
+ div.textContent = text;
1347
+ return div.innerHTML;
1348
+ }
1349
+
1350
+ // ═══════════════════════════════════════════════════════
1351
+ // COPY FUNCTIONS
1352
+ // ═══════════════════════════════════════════════════════
1353
+ function copyBlock(button, codeId) {
1354
+ const codeEl = document.getElementById(codeId);
1355
+ if (!codeEl) return;
1356
+ navigator.clipboard.writeText(codeEl.textContent).then(() => {
1357
+ button.textContent = '✓ Copied!';
1358
+ button.classList.add('copied');
1359
+ setTimeout(() => {
1360
+ button.textContent = '📋 Copy';
1361
+ button.classList.remove('copied');
1362
+ }, 2000);
1363
+ });
1364
+ }
1365
+
1366
+ function copyCode() {
1367
+ if (!state.lastCode) return;
1368
+ const btn = document.getElementById('btn-copy-code');
1369
+ navigator.clipboard.writeText(state.lastCode).then(() => {
1370
+ btn.textContent = '✓ Copied!';
1371
+ setTimeout(() => { btn.textContent = '📋 Copy'; }, 2000);
1372
+ });
1373
+ }
1374
+
1375
+ // ═══════════════════════════════════════════════════════
1376
+ // STATUS BAR
1377
+ // ═══════════════════════════════════════════════════════
1378
+ function renderStatus(text, statusState) {
1379
+ const indicator = document.getElementById('status-indicator');
1380
+ const textEl = document.getElementById('status-text');
1381
+ const dotEl = indicator.querySelector('.status-dot');
1382
+
1383
+ indicator.className = 'status-indicator';
1384
+ switch (statusState) {
1385
+ case 'working':
1386
+ indicator.classList.add('status-working');
1387
+ dotEl.textContent = '◐';
1388
+ break;
1389
+ case 'success':
1390
+ indicator.classList.add('status-success');
1391
+ dotEl.textContent = '✓';
1392
+ break;
1393
+ case 'error':
1394
+ indicator.classList.add('status-error');
1395
+ dotEl.textContent = '✗';
1396
+ break;
1397
+ case 'info':
1398
+ indicator.classList.add('status-info');
1399
+ dotEl.textContent = 'ℹ';
1400
+ break;
1401
+ default:
1402
+ indicator.classList.add('status-idle');
1403
+ dotEl.textContent = '●';
1404
+ }
1405
+ textEl.textContent = text || 'IDLE';
1406
+ }
1407
+
1408
+ // ═══════════════════════════════════════════════════════
1409
+ // OUTPUT PANEL
1410
+ // ═══════════════════════════════════════════════════════
1411
+ function switchTab(tab, { forcePreviewReload = false } = {}) {
1412
+ const wasPreview = state.activeTab === 'preview';
1413
+ state.activeTab = tab;
1414
+ document.querySelectorAll('.output-tab').forEach((btn) => {
1415
+ btn.classList.toggle('active', btn.dataset.tab === tab);
1416
+ });
1417
+ document.querySelectorAll('.tab-pane').forEach((pane) => {
1418
+ pane.classList.toggle('active', pane.id === `pane-${tab}`);
1419
+ });
1420
+ if (tab === 'preview') {
1421
+ ensureWebPreviewLoaded({ forceReload: forcePreviewReload || !wasPreview });
1422
+ }
1423
+ }
1424
+
1425
+ function renderExecution(execution) {
1426
+ if (!execution) return;
1427
+ state.lastExecution = execution;
1428
+
1429
+ // Console
1430
+ const stdout = execution.stdout || '';
1431
+ const stderr = execution.stderr || '';
1432
+ document.getElementById('console-stdout').textContent = stdout || 'No output.';
1433
+ document.getElementById('console-stderr').textContent = stderr || 'No errors.';
1434
+
1435
+ // Code
1436
+ if (execution.code) {
1437
+ state.lastCode = execution.code;
1438
+ state.lastCodeLang = execution.language || 'code';
1439
+ document.getElementById('code-tab-lang').textContent = state.lastCodeLang;
1440
+ document.getElementById('code-display').innerHTML = `<pre>${escapeHtml(execution.code)}</pre>`;
1441
+ }
1442
+
1443
+ // Download
1444
+ const dlBtn = document.getElementById('btn-download');
1445
+ if (execution.download_url) {
1446
+ dlBtn.href = execution.download_url;
1447
+ dlBtn.style.display = 'inline-flex';
1448
+ dlBtn.setAttribute('download', '');
1449
+ } else {
1450
+ dlBtn.style.display = 'none';
1451
+ }
1452
+
1453
+ // Preview
1454
+ const placeholder = document.getElementById('preview-placeholder');
1455
+ const img = document.getElementById('preview-image');
1456
+ const iframe = getPreviewIframe();
1457
+ const fsBtn = document.getElementById('btn-fullscreen');
1458
+
1459
+ if (execution.image_url) {
1460
+ placeholder.style.display = 'none';
1461
+ iframe.style.display = 'none';
1462
+ fsBtn.style.display = 'none';
1463
+ img.src = execution.image_url;
1464
+ img.style.display = 'block';
1465
+ if (state.activeTab !== 'console' && state.activeTab !== 'code') {
1466
+ switchTab('preview');
1467
+ }
1468
+ } else if (execution.target === 'web' && execution.code) {
1469
+ placeholder.style.display = 'none';
1470
+ img.style.display = 'none';
1471
+ iframe.style.display = 'block';
1472
+ fsBtn.style.display = 'block';
1473
+ state.pendingWebPreviewCode = execution.code;
1474
+ state.loadedWebPreviewCode = '';
1475
+ state.scheduledWebPreviewCode = '';
1476
+ if (state.activeTab !== 'console' && state.activeTab !== 'code') {
1477
+ switchTab('preview', { forcePreviewReload: true });
1478
+ } else {
1479
+ iframe.srcdoc = '';
1480
+ }
1481
+ } else {
1482
+ // No visual preview, but maybe there's stdout
1483
+ if (stdout || stderr) {
1484
+ // Auto-switch to suggested tab or console
1485
+ const suggested = execution.suggested_tab || 'console';
1486
+ switchTab(suggested);
1487
+ }
1488
+ }
1489
+ }
1490
+
1491
+ function resetOutput() {
1492
+ const iframe = getPreviewIframe();
1493
+ document.getElementById('preview-placeholder').style.display = '';
1494
+ document.getElementById('preview-image').style.display = 'none';
1495
+ iframe.style.display = 'none';
1496
+ iframe.srcdoc = '';
1497
+ document.getElementById('btn-fullscreen').style.display = 'none';
1498
+ document.getElementById('console-stdout').textContent = 'No output.';
1499
+ document.getElementById('console-stderr').textContent = 'No errors.';
1500
+ document.getElementById('code-display').innerHTML = '<div class="code-placeholder">No code generated yet.</div>';
1501
+ document.getElementById('code-tab-lang').textContent = '—';
1502
+ document.getElementById('btn-download').style.display = 'none';
1503
+ state.lastExecution = null;
1504
+ state.lastCode = '';
1505
+ state.lastCodeLang = '';
1506
+ state.pendingWebPreviewCode = '';
1507
+ state.loadedWebPreviewCode = '';
1508
+ state.scheduledWebPreviewCode = '';
1509
+ }
1510
+
1511
+ // ═══════════════════════════════════════════════════════
1512
+ // FULLSCREEN
1513
+ // ═══════════════════════════════════════════════════════
1514
+ function getPreviewIframe() {
1515
+ return document.getElementById('preview-iframe');
1516
+ }
1517
+
1518
+ function recreatePreviewIframe() {
1519
+ const oldFrame = getPreviewIframe();
1520
+ const freshFrame = document.createElement('iframe');
1521
+ freshFrame.id = 'preview-iframe';
1522
+ freshFrame.setAttribute('sandbox', 'allow-scripts');
1523
+ freshFrame.style.display = oldFrame.style.display || 'block';
1524
+ oldFrame.replaceWith(freshFrame);
1525
+ return freshFrame;
1526
+ }
1527
+
1528
+ function ensureWebPreviewLoaded({ forceReload = false } = {}) {
1529
+ const iframe = getPreviewIframe();
1530
+ if (!state.pendingWebPreviewCode || state.activeTab !== 'preview' || iframe.style.display === 'none') {
1531
+ return;
1532
+ }
1533
+ if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) {
1534
+ schedulePreviewResize(iframe);
1535
+ return;
1536
+ }
1537
+ if (!forceReload && state.scheduledWebPreviewCode === state.pendingWebPreviewCode) {
1538
+ return;
1539
+ }
1540
+
1541
+ state.scheduledWebPreviewCode = state.pendingWebPreviewCode;
1542
+ iframe.srcdoc = '';
1543
+ const loadWhenLaidOut = () => {
1544
+ if (state.activeTab !== 'preview' || !state.pendingWebPreviewCode) {
1545
+ state.scheduledWebPreviewCode = '';
1546
+ return;
1547
+ }
1548
+ if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) return;
1549
+ const visibleFrame = getPreviewIframe();
1550
+ const rect = visibleFrame.getBoundingClientRect();
1551
+ if (rect.width < 10 || rect.height < 10) {
1552
+ state.scheduledWebPreviewCode = '';
1553
+ setTimeout(() => ensureWebPreviewLoaded({ forceReload }), 50);
1554
+ return;
1555
+ }
1556
+ const freshFrame = recreatePreviewIframe();
1557
+ freshFrame.srcdoc = state.pendingWebPreviewCode;
1558
+ state.loadedWebPreviewCode = state.pendingWebPreviewCode;
1559
+ state.scheduledWebPreviewCode = '';
1560
+ schedulePreviewResize(freshFrame);
1561
+ };
1562
+ requestAnimationFrame(() => requestAnimationFrame(loadWhenLaidOut));
1563
+ setTimeout(loadWhenLaidOut, 75);
1564
+ }
1565
+
1566
+ function schedulePreviewResize(iframe) {
1567
+ const dispatchResize = () => {
1568
+ try {
1569
+ iframe.contentWindow?.dispatchEvent(new Event('resize'));
1570
+ } catch (_err) {
1571
+ // Sandboxed srcdoc frames may reject parent-origin access; visible-load timing is the main fix.
1572
+ }
1573
+ };
1574
+ requestAnimationFrame(() => {
1575
+ requestAnimationFrame(dispatchResize);
1576
+ });
1577
+ setTimeout(dispatchResize, 100);
1578
+ setTimeout(dispatchResize, 350);
1579
+ }
1580
+
1581
+ function observePreviewSize() {
1582
+ const previewPane = document.getElementById('pane-preview');
1583
+ if (!previewPane) return;
1584
+
1585
+ window.addEventListener('resize', () => {
1586
+ if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
1587
+ schedulePreviewResize(getPreviewIframe());
1588
+ }
1589
+ });
1590
+
1591
+ if (typeof ResizeObserver === 'undefined') return;
1592
+ const observer = new ResizeObserver(() => {
1593
+ if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
1594
+ schedulePreviewResize(getPreviewIframe());
1595
+ }
1596
+ });
1597
+ observer.observe(previewPane);
1598
+ }
1599
+
1600
+ function openFullscreen() {
1601
+ const overlay = document.getElementById('fullscreen-overlay');
1602
+ const iframe = document.getElementById('fullscreen-iframe');
1603
+ if (state.lastExecution && state.lastExecution.target === 'web' && state.lastExecution.code) {
1604
+ iframe.srcdoc = state.lastExecution.code;
1605
+ }
1606
+ overlay.classList.add('active');
1607
+ }
1608
+
1609
+ function closeFullscreen() {
1610
+ document.getElementById('fullscreen-overlay').classList.remove('active');
1611
+ document.getElementById('fullscreen-iframe').srcdoc = '';
1612
+ }
1613
+
1614
+ function handleFullscreenKeydown(event) {
1615
+ if (event.key !== 'Escape') return;
1616
+ const overlay = document.getElementById('fullscreen-overlay');
1617
+ if (!overlay.classList.contains('active')) return;
1618
+ event.preventDefault();
1619
+ closeFullscreen();
1620
+ }
1621
+
1622
+ // ═══════════════════════════════════════════════════════
1623
+ // SEND / RECEIVE
1624
+ // ═══════════════════════════════════════════════════════
1625
+ function handleSend() {
1626
+ const input = document.getElementById('chat-input');
1627
+ const prompt = input.value.trim();
1628
+ if (!prompt || state.isGenerating) return;
1629
+ input.value = '';
1630
+ autoResize();
1631
+ sendMessage(prompt);
1632
+ }
1633
+
1634
+ async function sendMessage(prompt) {
1635
+ if (state.isGenerating) return;
1636
+
1637
+ // UI updates
1638
+ state.isGenerating = true;
1639
+ toggleInputState(true);
1640
+ addUserMessage(prompt);
1641
+ addAssistantMessage();
1642
+ renderStatus('Thinking…', 'working');
1643
+
1644
+ const historyJSON = JSON.stringify(state.history);
1645
+ const execContextJSON = JSON.stringify(state.executionContext);
1646
+
1647
+ try {
1648
+ // Step 1: POST to get event_id
1649
+ const resp = await fetch('/gradio_api/call/chat', {
1650
+ method: 'POST',
1651
+ headers: { 'Content-Type': 'application/json' },
1652
+ body: JSON.stringify({
1653
+ data: [prompt, state.targetLanguage, historyJSON, execContextJSON]
1654
+ })
1655
+ });
1656
+
1657
+ if (!resp.ok) {
1658
+ throw new Error(`API error: ${resp.status} ${resp.statusText}`);
1659
+ }
1660
+
1661
+ const { event_id } = await resp.json();
1662
+
1663
+ // Step 2: Connect EventSource
1664
+ const eventSource = new EventSource(`/gradio_api/call/chat/${event_id}`);
1665
+ state.currentEventSource = eventSource;
1666
+
1667
+ let lastContent = '';
1668
+
1669
+ eventSource.addEventListener('generating', (e) => {
1670
+ try {
1671
+ const dataArray = JSON.parse(e.data);
1672
+ const payload = JSON.parse(dataArray[0]);
1673
+ handlePayload(payload, true);
1674
+ if (payload.history && payload.history.length > 0) {
1675
+ const lastMsg = payload.history[payload.history.length - 1];
1676
+ if (lastMsg.role === 'assistant') {
1677
+ lastContent = lastMsg.content;
1678
+ }
1679
+ }
1680
+ } catch (err) {
1681
+ console.error('Parse error (generating):', err);
1682
+ }
1683
+ });
1684
+
1685
+ eventSource.addEventListener('complete', (e) => {
1686
+ try {
1687
+ const dataArray = JSON.parse(e.data);
1688
+ const payload = JSON.parse(dataArray[0]);
1689
+ handlePayload(payload, false);
1690
+ } catch (err) {
1691
+ console.error('Parse error (complete):', err);
1692
+ }
1693
+ eventSource.close();
1694
+ onGenerationEnd();
1695
+ });
1696
+
1697
+ eventSource.addEventListener('error', (e) => {
1698
+ let errorMsg = 'An error occurred during generation.';
1699
+ if (e.data) {
1700
+ errorMsg = e.data;
1701
+ }
1702
+ console.error('SSE error:', errorMsg);
1703
+ finalizeAssistantMessage();
1704
+ addSystemMessage(`Error: ${errorMsg}`);
1705
+ renderStatus('Error', 'error');
1706
+ eventSource.close();
1707
+ onGenerationEnd();
1708
+ });
1709
+
1710
+ } catch (err) {
1711
+ console.error('Send error:', err);
1712
+ finalizeAssistantMessage();
1713
+ addSystemMessage(`Error: ${err.message}`);
1714
+ renderStatus('Error', 'error');
1715
+ onGenerationEnd();
1716
+ }
1717
+ }
1718
+
1719
+ function handlePayload(payload, isStreaming) {
1720
+ // Status
1721
+ if (payload.status_text) {
1722
+ renderStatus(payload.status_text, payload.status_state || 'working');
1723
+ }
1724
+
1725
+ // History
1726
+ if (payload.history) {
1727
+ state.history = payload.history;
1728
+ const lastMsg = payload.history[payload.history.length - 1];
1729
+ if (lastMsg && lastMsg.role === 'assistant') {
1730
+ updateAssistantMessage(lastMsg.content, isStreaming);
1731
+ }
1732
+ }
1733
+
1734
+ // Execution
1735
+ if (payload.execution) {
1736
+ renderExecution(payload.execution);
1737
+ // Persist execution context if needed
1738
+ if (payload.execution) {
1739
+ state.executionContext = payload.execution;
1740
+ }
1741
+ }
1742
+
1743
+ // Final state
1744
+ if (payload.type === 'complete') {
1745
+ finalizeAssistantMessage();
1746
+ renderStatus('Done', 'success');
1747
+ setTimeout(() => {
1748
+ if (!state.isGenerating) renderStatus('Idle', 'idle');
1749
+ }, 3000);
1750
+ }
1751
+
1752
+ if (payload.type === 'error') {
1753
+ finalizeAssistantMessage();
1754
+ addSystemMessage(`Error: ${payload.status_text || 'Unknown error'}`);
1755
+ renderStatus('Error', 'error');
1756
+ }
1757
+
1758
+ // Auto-switch tab
1759
+ if (payload.execution && payload.execution.suggested_tab) {
1760
+ switchTab(payload.execution.suggested_tab);
1761
+ }
1762
+ }
1763
+
1764
+ function onGenerationEnd() {
1765
+ state.isGenerating = false;
1766
+ state.currentEventSource = null;
1767
+ toggleInputState(false);
1768
+ }
1769
+
1770
+ function toggleInputState(generating) {
1771
+ const sendBtn = document.getElementById('btn-send');
1772
+ const stopBtn = document.getElementById('btn-stop');
1773
+ const input = document.getElementById('chat-input');
1774
+
1775
+ if (generating) {
1776
+ sendBtn.style.display = 'none';
1777
+ stopBtn.style.display = 'flex';
1778
+ input.disabled = true;
1779
+ input.placeholder = 'Generating…';
1780
+ } else {
1781
+ sendBtn.style.display = 'flex';
1782
+ stopBtn.style.display = 'none';
1783
+ sendBtn.disabled = false;
1784
+ input.disabled = false;
1785
+ input.placeholder = 'Ask a coding question or describe what to build…';
1786
+ input.focus();
1787
+ }
1788
+ }
1789
+
1790
+ function stopGeneration() {
1791
+ if (state.currentEventSource) {
1792
+ state.currentEventSource.close();
1793
+ state.currentEventSource = null;
1794
+ }
1795
+ finalizeAssistantMessage();
1796
+ addSystemMessage('Generation stopped by user.');
1797
+ renderStatus('Stopped', 'info');
1798
+ onGenerationEnd();
1799
+ }
1800
+
1801
+ function resetConversation(announcement) {
1802
+ state.history = [];
1803
+ state.executionContext = {};
1804
+ state.lastExecution = null;
1805
+ state.lastCode = '';
1806
+ state.lastCodeLang = '';
1807
+ state.reasoningExpanded = false;
1808
+
1809
+ if (state.currentEventSource) {
1810
+ state.currentEventSource.close();
1811
+ state.currentEventSource = null;
1812
+ }
1813
+ state.isGenerating = false;
1814
+ toggleInputState(false);
1815
+
1816
+ document.getElementById('chat-messages').innerHTML = '';
1817
+ resetOutput();
1818
+ switchTab('preview');
1819
+ renderStatus('Idle', 'idle');
1820
+ if (announcement) {
1821
+ addSystemMessage(announcement);
1822
+ }
1823
+ }
1824
+
1825
+ function newChat() {
1826
+ resetConversation(`Session reset. Welcome back to ${CONFIG.app_title || 'North Mini Code 1.0'}.`);
1827
+ }
1828
+ </script>
1829
+ </body>
1830
+ </html>