File size: 8,805 Bytes
4fb7e13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# cycles_chat_app.py
import os, math, numpy as np, matplotlib.pyplot as plt, gradio as gr
import openai

# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 0. OpenAI API key
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if "OPENAI_API_KEY" not in os.environ:
    os.environ["OPENAI_API_KEY"] = input("๐Ÿ”‘ Enter your OpenAI API key: ").strip()
openai.api_key = os.environ["OPENAI_API_KEY"]

# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 1. Wave-style chart utilities
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
CYCLES = {
    "Tech Cycle (50 yr)": 50,
    "Finance Cycle (80 yr)": 80,
    "Hegemony Cycle (250 yr)": 250,
}
COLOR_MAP     = {50: "#ff3333", 80: "#ffcc00", 250: "#66ccff"}
AMPLITUDE_MAP = {50: 1.0,      80: 1.6,       250: 4.0}
CENTER = 2025                                                      # alignment reference

def _half_sine(xs, period, amp):
    """positive half-sine wave centred on CENTER"""
    phase = np.mod(xs - CENTER, period)
    y = amp * np.sin(np.pi * phase / period)
    y[y < 0] = 0
    return y

def build_wave_chart_and_summary(start: int, end: int):
    xs = np.linspace(start, end, (end - start) * 4)                # 4 pts per year
    fig, ax = plt.subplots(figsize=(14, 6))
    fig.subplots_adjust(top=0.9)                                   # spare margin for labels

    summaries, align_years, all_year_labels = [], None, set()

    # draw half-sine โ€œtowersโ€
    for period in sorted(CYCLES.values()):
        col, amp = COLOR_MAP[period], AMPLITUDE_MAP[period]
        for frac in np.linspace(amp / 30, amp, 30):
            ax.plot(xs, _half_sine(xs, period, frac),
                    color=col, alpha=0.85, lw=0.6)

        years = [CENTER + n * period for n in range(
                 math.ceil((start - CENTER) / period),
                 math.floor((end   - CENTER) / period) + 1)]
        summaries.append(f"{period}-yr cycle peaks: {years}")
        align_years = set(years) if align_years is None else align_years & set(years)
        all_year_labels.update(years)

    # baseline small year labels (duplicates removed)
    for y in sorted(all_year_labels):
        if start <= y <= end:
            ax.text(y, -0.1, str(y), ha="center", va="top",
                    fontsize=6, color="white", rotation=90)

    # styling
    ax.set_facecolor("black"); fig.patch.set_facecolor("black")
    ax.set_xlim(start, end)
    ax.set_ylim(-0.2, max(AMPLITUDE_MAP.values()) + 0.2)
    ax.set_xlabel("Year", color="white")
    ax.set_ylabel("Relative Amplitude", color="white")
    # (no chart title per request)
    ax.tick_params(colors="white")
    for spine in ax.spines.values():
        spine.set_color("white")
    ax.grid(axis="y", color="white", ls="--", lw=.3, alpha=.3)

    # alignment marker (CENTER) and 250-yr arrow
    ax.axvline(CENTER, color="white", ls="--", lw=1, alpha=.6)
    arrow_y = AMPLITUDE_MAP[250] * 1.05
    ax.annotate("", xy=(CENTER - 125, arrow_y), xytext=(CENTER + 125, arrow_y),
                arrowprops=dict(arrowstyle="<|-|>", color="white", lw=1.2))
    ax.text(CENTER, arrow_y + .15, "250 yr", color="white",
            ha="center", va="bottom", fontsize=10, fontweight="bold")

    summary = (f"Range {start}-{end}\n"
               + "\n".join(summaries)
               + "\nAlignment year inside range: "
               + (", ".join(map(str, sorted(align_years))) if align_years else "None"))
    return fig, summary

# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 2. GPT chat helper
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
BASE_PROMPT = (
    "You are a concise and accurate Korean assistant. "
    "Always incorporate the provided [Chart summary] when replying."
)

def chat_with_gpt(history, user_msg, chart_summary):
    msgs = [{"role": "system", "content": BASE_PROMPT}]
    if chart_summary != "No chart yet.":
        msgs.append({"role": "system", "content": f"[Chart summary]\n{chart_summary}"})

    for u, a in history:
        msgs += [{"role": "user", "content": u}, {"role": "assistant", "content": a}]
    msgs.append({"role": "user", "content": user_msg})

    reply = openai.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=msgs,
        max_tokens=600,
        temperature=0.7
    ).choices[0].message.content.strip()
    return reply

# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 3. Gradio UI
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
custom_css = """
#wave_plot {width: 100% !important;}
#wave_plot canvas {width: 100% !important; height: auto !important;}
"""

with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
    # Service title and descriptions
    gr.Markdown("## ๐Ÿš€ **TriPulse Navigator**")
    gr.Markdown(
        """
**Tech Cycle (50 yr)** โ€“ Innovation booms and busts about every half-century.  
**Finance Cycle (80 yr)** โ€“ Credit expansions and crises roughly once per lifetime.  
**Hegemony Cycle (250 yr)** โ€“ Rise and decline of world-leading powers across centuries.
"""
    )

    chart_summary_state = gr.State(value="No chart yet.")

    with gr.Tabs():
        # โ–ธ Tab 1 โ€” Timeline Chart
        with gr.TabItem("Timeline Chart"):
            # initial chart
            fig0, summ0 = build_wave_chart_and_summary(1500, 2500)
            plot_out = gr.Plot(value=fig0, elem_id="wave_plot")

            # inputs and zoom buttons
            with gr.Row():
                start_year = gr.Number(label="Start Year", value=1500)
                end_year   = gr.Number(label="End Year",   value=2500)
                zoom_in_btn  = gr.Button("๐Ÿ” Zoom In")
                zoom_out_btn = gr.Button("๐Ÿ”Ž Zoom Out")

            # refresh on manual year change
            def refresh_chart(s, e):
                fig, summ = build_wave_chart_and_summary(int(s), int(e))
                return fig, summ

            start_year.change(refresh_chart, [start_year, end_year],
                              [plot_out, chart_summary_state])
            end_year.change(refresh_chart,   [start_year, end_year],
                              [plot_out, chart_summary_state])

            # zoom helpers
            def zoom(s, e, factor):
                mid  = (s + e) / 2
                span = (e - s) * factor / 2
                new_s, new_e = int(mid - span), int(mid + span)
                fig, summ = build_wave_chart_and_summary(new_s, new_e)
                return new_s, new_e, fig, summ

            zoom_in_btn.click(
                lambda s, e: zoom(s, e, 0.5),
                inputs=[start_year, end_year],
                outputs=[start_year, end_year, plot_out, chart_summary_state],
            )
            zoom_out_btn.click(
                lambda s, e: zoom(s, e, 2.0),
                inputs=[start_year, end_year],
                outputs=[start_year, end_year, plot_out, chart_summary_state],
            )

        # โ–ธ Tab 2 โ€” GPT Chat
        with gr.TabItem("GPT Chat"):
            chatbot = gr.Chatbot(label="Assistant")
            user_in = gr.Textbox(lines=3, placeholder="๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”โ€ฆ")
            send_btn = gr.Button("Send", variant="primary")

            def respond(chat_hist, user_msg, summary):
                ans = chat_with_gpt(chat_hist, user_msg, summary)
                chat_hist.append((user_msg, ans))
                return chat_hist, gr.Textbox(value="", interactive=True)

            send_btn.click(respond, [chatbot, user_in, chart_summary_state],
                                       [chatbot, user_in])
            user_in.submit(respond, [chatbot, user_in, chart_summary_state],
                                       [chatbot, user_in])

if __name__ == "__main__":
    demo.launch()