openfree commited on
Commit
fed463b
ยท
verified ยท
1 Parent(s): 184d70e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +216 -38
app.py CHANGED
@@ -10,52 +10,62 @@ if "OPENAI_API_KEY" not in os.environ:
10
  openai.api_key = os.environ["OPENAI_API_KEY"]
11
 
12
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
- # 1. Wave-style chart utilities (โ† NEW)
14
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
15
  CYCLES = {
16
  "Tech Cycle (50 yr)": 50,
17
  "Finance Cycle (80 yr)": 80,
18
  "Hegemony Cycle (250 yr)": 250,
19
  }
20
- COLOR_MAP = {50: "#ff3333", 80: "#ffcc00", 250: "#66ccff"} # red / yellow / blue
21
- AMPLITUDE_MAP = {50: 1.0, 80: 1.6, 250: 4.0} # taller tower for longer cycle
22
- CENTER = 2025 # alignment reference
23
 
24
  def _half_sine(xs, period, amp):
25
- """positive half-sine wave, aligned so peak at CENTER, 0 elsewhere"""
26
  phase = np.mod(xs - CENTER, period)
27
  y = amp * np.sin(np.pi * phase / period)
28
  y[y < 0] = 0
29
  return y
30
 
31
  def build_wave_chart_and_summary(start: int, end: int):
32
- """return (matplotlib figure, text summary)"""
33
- xs = np.linspace(start, end, (end - start) * 4) # 4 pts/yr โ‡’ smooth
34
- fig, ax = plt.subplots(figsize=(10, 3))
35
- summaries, align_years = [], None
 
36
 
37
- # draw many stacked half-sine lines for each cycle โ†’ โ€œtowerโ€ ๋А๋‚Œ
38
  for period in sorted(CYCLES.values()):
39
  col, amp = COLOR_MAP[period], AMPLITUDE_MAP[period]
40
- for frac in np.linspace(amp / 30, amp, 30): # 30 concentric lines
41
- ax.plot(xs, _half_sine(xs, period, frac),
42
- color=col, alpha=0.85, lw=0.6)
43
 
44
  years = [CENTER + n * period for n in range(
45
  math.ceil((start - CENTER) / period),
46
  math.floor((end - CENTER) / period) + 1)]
47
  summaries.append(f"{period}-yr cycle peaks: {years}")
48
  align_years = set(years) if align_years is None else align_years & set(years)
 
 
 
 
 
 
 
49
 
50
- # cosmeticโ€Šโ€”โ€Šblack bg & white grid
51
- ax.set_facecolor("black"); fig.patch.set_facecolor("black")
52
- ax.set_xlim(start, end); ax.set_ylim(0, max(AMPLITUDE_MAP.values()) + .2)
53
- ax.set_xlabel("Year", color="white"); ax.set_ylabel("Relative Amplitude", color="white")
54
- ax.set_title("Technology ยท Finance ยท Hegemony Cycles", color="white", pad=20)
55
- ax.tick_params(colors="white"); [s.set_color("white") for s in ax.spines.values()]
56
- ax.grid(color="white", ls="--", lw=.3, alpha=.3)
 
 
 
 
 
57
 
58
- # vertical dashed line at alignment year + 250-yr span arrow
59
  ax.axvline(CENTER, color="white", ls="--", lw=1, alpha=.6)
60
  arrow_y = AMPLITUDE_MAP[250] * 1.05
61
  ax.annotate("", xy=(CENTER - 125, arrow_y), xytext=(CENTER + 125, arrow_y),
@@ -63,13 +73,14 @@ def build_wave_chart_and_summary(start: int, end: int):
63
  ax.text(CENTER, arrow_y + .15, "250 yr", color="white",
64
  ha="center", va="bottom", fontsize=10, fontweight="bold")
65
 
66
- summary = (f"Range {start}-{end}\n" +
67
- "\n".join(summaries) +
68
- f"\nAlignment year inside range: {', '.join(map(str, sorted(align_years))) if align_years else 'None'}")
 
69
  return fig, summary
70
 
71
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
72
- # 2. GPT chat (์ฐจํŠธ ์š”์•ฝ โ†’ system ํ”„๋กฌํ”„ํŠธ์— ์ž๋™ ์‚ฝ์ž…)
73
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
74
  BASE_PROMPT = (
75
  "๋‹น์‹ ์€ ๊ฐ„๊ฒฐํ•˜๊ณ  ์ •ํ™•ํ•œ ํ•œ๊ตญ์–ด ์–ด์‹œ์Šคํ„ดํŠธ์ž…๋‹ˆ๋‹ค. "
@@ -78,22 +89,31 @@ BASE_PROMPT = (
78
 
79
  def chat_with_gpt(history, user_msg, chart_summary):
80
  msgs = [{"role": "system", "content": BASE_PROMPT}]
81
- if chart_summary != "No chart yet.": # ์ฐจํŠธ๊ฐ€ ์ด๋ฏธ ๊ทธ๋ ค์กŒ๋‹ค๋ฉด
82
  msgs.append({"role": "system", "content": f"[Chart summary]\n{chart_summary}"})
83
 
84
  for u, a in history:
85
- msgs += [{"role": "user", "content": u}, {"role": "assistant", "content": a}]
 
86
  msgs.append({"role": "user", "content": user_msg})
87
 
88
- answer = openai.chat.completions.create(
89
- model="gpt-3.5-turbo", messages=msgs,
90
- max_tokens=600, temperature=0.7).choices[0].message.content.strip()
91
- return answer
 
 
 
92
 
93
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
94
- # 3. Gradio UI
95
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
96
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
 
 
 
 
 
97
  gr.Markdown("## ๐Ÿ“Š Wave-style Cycle Timeline & ๐Ÿ’ฌ GPT Chat")
98
 
99
  chart_summary_state = gr.State(value="No chart yet.")
@@ -101,11 +121,169 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
101
  with gr.Tabs():
102
  # โ–ธ Tab 1 โ€” Chart
103
  with gr.TabItem("Timeline Chart"):
104
- start_year = gr.Number(label="Start Year", value=1500)
105
- end_year = gr.Number(label="End Year", value=2500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  fig0, summ0 = build_wave_chart_and_summary(1500, 2500)
108
- plot_out = gr.Plot(value=fig0)
 
 
 
 
109
 
110
  def refresh_chart(s, e):
111
  fig, summ = build_wave_chart_and_summary(int(s), int(e))
@@ -123,8 +301,8 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
123
  send_btn = gr.Button("Send", variant="primary")
124
 
125
  def respond(chat_hist, user_msg, summary):
126
- reply = chat_with_gpt(chat_hist, user_msg, summary)
127
- chat_hist.append((user_msg, reply))
128
  return chat_hist, gr.Textbox(value="", interactive=True)
129
 
130
  send_btn.click(respond, [chatbot, user_in, chart_summary_state],
 
10
  openai.api_key = os.environ["OPENAI_API_KEY"]
11
 
12
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
+ # 1. Wave-style chart utilities
14
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
15
  CYCLES = {
16
  "Tech Cycle (50 yr)": 50,
17
  "Finance Cycle (80 yr)": 80,
18
  "Hegemony Cycle (250 yr)": 250,
19
  }
20
+ COLOR_MAP = {50: "#ff3333", 80: "#ffcc00", 250: "#66ccff"}
21
+ AMPLITUDE_MAP = {50: 1.0, 80: 1.6, 250: 4.0}
22
+ CENTER = 2025 # alignment reference
23
 
24
  def _half_sine(xs, period, amp):
 
25
  phase = np.mod(xs - CENTER, period)
26
  y = amp * np.sin(np.pi * phase / period)
27
  y[y < 0] = 0
28
  return y
29
 
30
  def build_wave_chart_and_summary(start: int, end: int):
31
+ xs = np.linspace(start, end, (end - start) * 4)
32
+ fig, ax = plt.subplots(figsize=(14, 6))
33
+ fig.subplots_adjust(top=0.9) # prevent title clipping
34
+
35
+ summaries, align_years, all_year_labels = [], None, set()
36
 
 
37
  for period in sorted(CYCLES.values()):
38
  col, amp = COLOR_MAP[period], AMPLITUDE_MAP[period]
39
+ for frac in np.linspace(amp / 30, amp, 30):
40
+ ax.plot(xs, _half_sine(xs, period, frac), color=col, alpha=0.85, lw=0.6)
 
41
 
42
  years = [CENTER + n * period for n in range(
43
  math.ceil((start - CENTER) / period),
44
  math.floor((end - CENTER) / period) + 1)]
45
  summaries.append(f"{period}-yr cycle peaks: {years}")
46
  align_years = set(years) if align_years is None else align_years & set(years)
47
+ all_year_labels.update(years)
48
+
49
+ # baseline year labels (small font, duplicates removed)
50
+ for y in sorted(all_year_labels):
51
+ if start <= y <= end:
52
+ ax.text(y, -0.1, str(y), ha="center", va="top",
53
+ fontsize=6, color="white", rotation=90)
54
 
55
+ ax.set_facecolor("black")
56
+ fig.patch.set_facecolor("black")
57
+ ax.set_xlim(start, end)
58
+ ax.set_ylim(-0.2, max(AMPLITUDE_MAP.values()) + 0.2)
59
+ ax.set_xlabel("Year", color="white")
60
+ ax.set_ylabel("Relative Amplitude", color="white")
61
+ ax.set_title("Technology ยท Finance ยท Hegemony Cycles", color="white",
62
+ pad=35, fontsize=14, fontweight="bold")
63
+ ax.tick_params(colors="white")
64
+ for spine in ax.spines.values():
65
+ spine.set_color("white")
66
+ ax.grid(axis="y", color="white", ls="--", lw=.3, alpha=.3)
67
 
68
+ # alignment marker
69
  ax.axvline(CENTER, color="white", ls="--", lw=1, alpha=.6)
70
  arrow_y = AMPLITUDE_MAP[250] * 1.05
71
  ax.annotate("", xy=(CENTER - 125, arrow_y), xytext=(CENTER + 125, arrow_y),
 
73
  ax.text(CENTER, arrow_y + .15, "250 yr", color="white",
74
  ha="center", va="bottom", fontsize=10, fontweight="bold")
75
 
76
+ summary = (f"Range {start}-{end}\n"
77
+ + "\n".join(summaries)
78
+ + f"\nAlignment year inside range: "
79
+ + (", ".join(map(str, sorted(align_years))) if align_years else "None"))
80
  return fig, summary
81
 
82
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
83
+ # 2. GPT chat
84
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
85
  BASE_PROMPT = (
86
  "๋‹น์‹ ์€ ๊ฐ„๊ฒฐํ•˜๊ณ  ์ •ํ™•ํ•œ ํ•œ๊ตญ์–ด ์–ด์‹œ์Šคํ„ดํŠธ์ž…๋‹ˆ๋‹ค. "
 
89
 
90
  def chat_with_gpt(history, user_msg, chart_summary):
91
  msgs = [{"role": "system", "content": BASE_PROMPT}]
92
+ if chart_summary != "No chart yet.":
93
  msgs.append({"role": "system", "content": f"[Chart summary]\n{chart_summary}"})
94
 
95
  for u, a in history:
96
+ msgs += [{"role": "user", "content": u},
97
+ {"role": "assistant", "content": a}]
98
  msgs.append({"role": "user", "content": user_msg})
99
 
100
+ reply = openai.chat.completions.create(
101
+ model="gpt-3.5-turbo",
102
+ messages=msgs,
103
+ max_tokens=600,
104
+ temperature=0.7
105
+ ).choices[0].message.content.strip()
106
+ return reply
107
 
108
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
109
+ # 3. Gradio UI + CSS
110
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
111
+ custom_css = """
112
+ #wave_plot {width: 100% !important;}
113
+ #wave_plot canvas {width: 100% !important; height: auto !important;}
114
+ """
115
+
116
+ with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
117
  gr.Markdown("## ๐Ÿ“Š Wave-style Cycle Timeline & ๐Ÿ’ฌ GPT Chat")
118
 
119
  chart_summary_state = gr.State(value="No chart yet.")
 
121
  with gr.Tabs():
122
  # โ–ธ Tab 1 โ€” Chart
123
  with gr.TabItem("Timeline Chart"):
124
+ fig0, summ0 = build_wave_chart_and_summary(1500, 2500)
125
+ plot_out = gr.Plot(value=fig0, elem_id="wave_plot")
126
+
127
+ with gr.Row():
128
+ start_year = gr.Number(label="Start Year", value=1500)
129
+ end_year = gr.Number(label="End Year", value=2500)
130
+
131
+ def refresh_chart(s, e):
132
+ fig, summ = build_wave_chart_and_summary(int(s), int(e))
133
+ return fig, summ
134
+
135
+ start_year.change(refresh_chart, [start_year, end_year],
136
+ [plot_out, chart_summary_state])
137
+ end_year.change(refresh_chart, [start_year, end_year],
138
+ [plot_out, chart_summary_state])
139
+
140
+ # โ–ธ Tab 2 โ€” GPT Chat
141
+ with gr.TabItem("GPT Chat"):
142
+ chatbot = gr.Chatbot(label="Assistant")
143
+ user_in = gr.Textbox(lines=3, placeholder="๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”โ€ฆ")
144
+ send_btn = gr.Button("Send", variant="primary")
145
+
146
+ def respond(chat_hist, user_msg, summary):
147
+ ans = chat_with_gpt(chat_hist, user_msg, summary)
148
+ chat_hist.append((user_msg, ans))
149
+ return chat_hist, gr.Textbox(value="", interactive=True)
150
+
151
+ send_btn.click(respond, [chatbot, user_in, chart_summary_state],
152
+ [chatbot, user_in])
153
+ user_in.submit(respond, [chatbot, user_in, chart_summary_state],
154
+ [chatbot, user_in])
155
+
156
+ if __name__ == "__main__":
157
+ demo.launch()
158
+ # cycles_chat_app.py
159
+ import os, math, numpy as np, matplotlib.pyplot as plt, gradio as gr
160
+ import openai
161
+
162
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
163
+ # 0. OpenAI key
164
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
165
+ if "OPENAI_API_KEY" not in os.environ:
166
+ os.environ["OPENAI_API_KEY"] = input("๐Ÿ”‘ Enter your OpenAI API key: ").strip()
167
+ openai.api_key = os.environ["OPENAI_API_KEY"]
168
+
169
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
170
+ # 1. Wave-style chart utilities
171
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
172
+ CYCLES = {
173
+ "Tech Cycle (50 yr)": 50,
174
+ "Finance Cycle (80 yr)": 80,
175
+ "Hegemony Cycle (250 yr)": 250,
176
+ }
177
+ COLOR_MAP = {50: "#ff3333", 80: "#ffcc00", 250: "#66ccff"}
178
+ AMPLITUDE_MAP = {50: 1.0, 80: 1.6, 250: 4.0}
179
+ CENTER = 2025 # alignment reference
180
+
181
+ def _half_sine(xs, period, amp):
182
+ phase = np.mod(xs - CENTER, period)
183
+ y = amp * np.sin(np.pi * phase / period)
184
+ y[y < 0] = 0
185
+ return y
186
+
187
+ def build_wave_chart_and_summary(start: int, end: int):
188
+ xs = np.linspace(start, end, (end - start) * 4)
189
+ fig, ax = plt.subplots(figsize=(14, 6))
190
+ fig.subplots_adjust(top=0.9) # prevent title clipping
191
+
192
+ summaries, align_years, all_year_labels = [], None, set()
193
+
194
+ for period in sorted(CYCLES.values()):
195
+ col, amp = COLOR_MAP[period], AMPLITUDE_MAP[period]
196
+ for frac in np.linspace(amp / 30, amp, 30):
197
+ ax.plot(xs, _half_sine(xs, period, frac), color=col, alpha=0.85, lw=0.6)
198
+
199
+ years = [CENTER + n * period for n in range(
200
+ math.ceil((start - CENTER) / period),
201
+ math.floor((end - CENTER) / period) + 1)]
202
+ summaries.append(f"{period}-yr cycle peaks: {years}")
203
+ align_years = set(years) if align_years is None else align_years & set(years)
204
+ all_year_labels.update(years)
205
+
206
+ # baseline year labels (small font, duplicates removed)
207
+ for y in sorted(all_year_labels):
208
+ if start <= y <= end:
209
+ ax.text(y, -0.1, str(y), ha="center", va="top",
210
+ fontsize=6, color="white", rotation=90)
211
+
212
+ ax.set_facecolor("black")
213
+ fig.patch.set_facecolor("black")
214
+ ax.set_xlim(start, end)
215
+ ax.set_ylim(-0.2, max(AMPLITUDE_MAP.values()) + 0.2)
216
+ ax.set_xlabel("Year", color="white")
217
+ ax.set_ylabel("Relative Amplitude", color="white")
218
+ ax.set_title("Technology ยท Finance ยท Hegemony Cycles", color="white",
219
+ pad=35, fontsize=14, fontweight="bold")
220
+ ax.tick_params(colors="white")
221
+ for spine in ax.spines.values():
222
+ spine.set_color("white")
223
+ ax.grid(axis="y", color="white", ls="--", lw=.3, alpha=.3)
224
+
225
+ # alignment marker
226
+ ax.axvline(CENTER, color="white", ls="--", lw=1, alpha=.6)
227
+ arrow_y = AMPLITUDE_MAP[250] * 1.05
228
+ ax.annotate("", xy=(CENTER - 125, arrow_y), xytext=(CENTER + 125, arrow_y),
229
+ arrowprops=dict(arrowstyle="<|-|>", color="white", lw=1.2))
230
+ ax.text(CENTER, arrow_y + .15, "250 yr", color="white",
231
+ ha="center", va="bottom", fontsize=10, fontweight="bold")
232
+
233
+ summary = (f"Range {start}-{end}\n"
234
+ + "\n".join(summaries)
235
+ + f"\nAlignment year inside range: "
236
+ + (", ".join(map(str, sorted(align_years))) if align_years else "None"))
237
+ return fig, summary
238
 
239
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
240
+ # 2. GPT chat
241
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
242
+ BASE_PROMPT = (
243
+ "๋‹น์‹ ์€ ๊ฐ„๊ฒฐํ•˜๊ณ  ์ •ํ™•ํ•œ ํ•œ๊ตญ์–ด ์–ด์‹œ์Šคํ„ดํŠธ์ž…๋‹ˆ๋‹ค. "
244
+ "์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ๋‹ตํ•  ๋•Œ, ์ œ๊ณต๋œ [Chart summary] ๋‚ด์šฉ์„ ๋ฐ˜๋“œ์‹œ ๋ฐ˜์˜ํ•˜์„ธ์š”."
245
+ )
246
+
247
+ def chat_with_gpt(history, user_msg, chart_summary):
248
+ msgs = [{"role": "system", "content": BASE_PROMPT}]
249
+ if chart_summary != "No chart yet.":
250
+ msgs.append({"role": "system", "content": f"[Chart summary]\n{chart_summary}"})
251
+
252
+ for u, a in history:
253
+ msgs += [{"role": "user", "content": u},
254
+ {"role": "assistant", "content": a}]
255
+ msgs.append({"role": "user", "content": user_msg})
256
+
257
+ reply = openai.chat.completions.create(
258
+ model="gpt-3.5-turbo",
259
+ messages=msgs,
260
+ max_tokens=600,
261
+ temperature=0.7
262
+ ).choices[0].message.content.strip()
263
+ return reply
264
+
265
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
266
+ # 3. Gradio UI + CSS
267
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
268
+ custom_css = """
269
+ #wave_plot {width: 100% !important;}
270
+ #wave_plot canvas {width: 100% !important; height: auto !important;}
271
+ """
272
+
273
+ with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
274
+ gr.Markdown("## ๐Ÿ“Š Wave-style Cycle Timeline & ๐Ÿ’ฌ GPT Chat")
275
+
276
+ chart_summary_state = gr.State(value="No chart yet.")
277
+
278
+ with gr.Tabs():
279
+ # โ–ธ Tab 1 โ€” Chart
280
+ with gr.TabItem("Timeline Chart"):
281
  fig0, summ0 = build_wave_chart_and_summary(1500, 2500)
282
+ plot_out = gr.Plot(value=fig0, elem_id="wave_plot")
283
+
284
+ with gr.Row():
285
+ start_year = gr.Number(label="Start Year", value=1500)
286
+ end_year = gr.Number(label="End Year", value=2500)
287
 
288
  def refresh_chart(s, e):
289
  fig, summ = build_wave_chart_and_summary(int(s), int(e))
 
301
  send_btn = gr.Button("Send", variant="primary")
302
 
303
  def respond(chat_hist, user_msg, summary):
304
+ ans = chat_with_gpt(chat_hist, user_msg, summary)
305
+ chat_hist.append((user_msg, ans))
306
  return chat_hist, gr.Textbox(value="", interactive=True)
307
 
308
  send_btn.click(respond, [chatbot, user_in, chart_summary_state],