File size: 9,027 Bytes
2aef962
4fb7e13
 
 
 
 
 
 
 
 
 
 
 
 
 
2aef962
 
 
 
4fb7e13
2aef962
 
 
4fb7e13
 
 
 
 
 
 
 
2aef962
4fb7e13
2aef962
4fb7e13
 
 
2aef962
4fb7e13
 
 
 
 
 
 
 
2aef962
 
4fb7e13
 
 
2aef962
4fb7e13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2aef962
4fb7e13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2aef962
 
 
 
4fb7e13
2aef962
f4534af
2aef962
f4534af
2aef962
 
 
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
# cycles_chat_app.py  โ€”  CycleNavigator (4-Cycle ๋ฒ„์ „)
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 = {
    "K-Wave (50 yr)":               50,   # ์žฅ๊ธฐ ์ฝ˜๋“œ๋ผํ‹ฐ์—ํ”„ ํŒŒ๋™
    "Business Cycle (Juglar 9 yr)":  9,   # ์„ค๋น„ํˆฌ์žยท์‹ ์šฉ ๊ฒฝ๊ธฐ์ˆœํ™˜
    "Finance Cycle (80 yr)":        80,   # ์‹ ์šฉยท๊ธˆ์œต ์žฅ์ฃผ๊ธฐ
    "Hegemony Cycle (250 yr)":     250,   # ํŒจ๊ถŒ ํฅ๋ง
}
COLOR_MAP     = {50: "#ff3333",  9: "#66ff66", 80: "#ffcc00", 250: "#66ccff"}
AMPLITUDE_MAP = {50: 1.0,        9: 0.6,      80: 1.6,        250: 4.0}
CENTER = 2025  # alignment reference

def _half_sine(xs, period, amp):
    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)
    fig, ax = plt.subplots(figsize=(14, 6))
    fig.subplots_adjust(top=0.9)

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

    for period in sorted(set(CYCLES.values())):  # iterate periods in ascending order
        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)]
        name = [k for k, v in CYCLES.items() if v == period][0]
        summaries.append(f"{name} peaks: {years}")
        align_years = set(years) if align_years is None else align_years & set(years)
        all_year_labels.update(years)

    # small baseline labels
    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)

    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")
    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
    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:
    # โ”€โ”€ ์„œ๋น„์Šค ์ œ๋ชฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    gr.Markdown("## ๐Ÿ”ญ **CycleNavigator**")

    # โ”€โ”€ ๋„ค ์‚ฌ์ดํด ๊ฐ„๋‹จ ์„ค๋ช… (์ž‘์€ ๊ธ€์”จ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    gr.Markdown(
        "<sub>"

        "**Business (Juglar 9 yr)** โ€“ Credit-driven capital investment booms & busts.โ€ƒ"
        "**K-Wave (50 yr)** โ€“ Long-wave industrial & technological shifts.โ€ƒ"        
        "**Finance (80 yr)** โ€“ Extended credit cycles culminating in crises.โ€ƒ"
        "**Hegemony (250 yr)** โ€“ Rise & decline of world powers."
        "</sub>"
    )

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

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

            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")

            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])

            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()