Skip to content

Commit dee3a64

Browse files
authored
Merge pull request #10 from Netdex/openai-responses
OpenAI responses
2 parents 64ba12f + daaddbd commit dee3a64

15 files changed

Lines changed: 876 additions & 57 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# User settings
2+
.vscode/
3+
.claude/
4+
15
# niinii runtime files
26
/data
37
imgui.ini

niinii/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ openai = { path = "../openai" }
7777
imgui-dx11-renderer = { path = "../third-party/imgui-dx11-renderer" }
7878
vvcore = { path = "../third-party/vvcore", optional = true }
7979

80-
winapi = { version = "0.3", features=["winuser"] }
80+
winapi = { version = "0.3", features = ["winuser"] }
8181
wio = "0.2"
8282
# detour = { version = "0.8", default-features = false }
8383

niinii/src/app.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ use crate::{
1010
parser::{self, Parser, SyntaxTree},
1111
renderer::context::{Context, ContextFlags},
1212
settings::{Settings, TranslatorType},
13-
support::{docking::UiDocking, platform::get_scroll_lock},
13+
support::docking::UiDocking,
1414
translator::{
1515
self, chat::ChatTranslator, deepl::DeepLTranslator, realtime::RealtimeTranslator,
16-
Translation, Translator,
16+
responses::ResponsesTranslator, Translation, Translator,
1717
},
1818
tts::{self, TtsEngine},
1919
view::{
@@ -60,6 +60,7 @@ pub struct App {
6060
show_style_editor: bool,
6161
show_inject: bool,
6262
show_translator: bool,
63+
no_inputs: bool,
6364

6465
settings: Settings,
6566
state: State,
@@ -78,6 +79,7 @@ impl App {
7879
TranslatorType::DeepL => Arc::new(DeepLTranslator),
7980
TranslatorType::Chat => Arc::new(ChatTranslator::new(&settings).await),
8081
TranslatorType::Realtime => Arc::new(RealtimeTranslator::new(&settings).await),
82+
TranslatorType::Responses => Arc::new(ResponsesTranslator::new(&settings).await),
8183
};
8284
let tts = TtsEngine::new(&settings);
8385
App {
@@ -91,6 +93,7 @@ impl App {
9193
show_style_editor: false,
9294
show_inject: false,
9395
show_translator: false,
96+
no_inputs: false,
9497
settings,
9598
state: State::Completed,
9699
error: None,
@@ -252,6 +255,9 @@ impl App {
252255
if ui.menu_item("Settings") {
253256
self.show_settings = true;
254257
}
258+
ui.separator();
259+
ui.menu_item_config("Disable interaction")
260+
.build_with_ref(&mut self.no_inputs);
255261
}
256262
if let Some(_menu) = ui.begin_menu("Gloss") {
257263
self.gloss.show_menu(ctx, ui);
@@ -298,22 +304,46 @@ impl App {
298304
});
299305
}
300306

307+
fn show_input_toggle(&mut self, ui: &Ui) -> bool {
308+
let mut hovered = false;
309+
ui.window("Interaction")
310+
.always_auto_resize(true)
311+
.movable(false)
312+
.collapsible(false)
313+
.resizable(false)
314+
.title_bar(false)
315+
.position([12.0, 12.0], Condition::Always)
316+
.bg_alpha(0.75)
317+
.build(|| {
318+
ui.text("Interaction disabled");
319+
if ui.button_with_size("Enable interaction", [180.0, 0.0]) {
320+
self.no_inputs = false;
321+
}
322+
hovered = ui.is_window_hovered();
323+
});
324+
hovered
325+
}
326+
301327
pub fn ui(&mut self, ctx: &mut Context, ui: &mut Ui, run: &mut bool) {
302328
if self.settings().overlay_mode
303329
&& !ctx.flags().contains(ContextFlags::SHARED_RENDER_CONTEXT)
304330
{
305331
ui.dockspace_over_viewport();
306332
};
307333

308-
let no_inputs = get_scroll_lock();
334+
let no_inputs = self.no_inputs;
335+
let mut toggle_hovered = false;
336+
if no_inputs {
337+
toggle_hovered = self.show_input_toggle(ui);
338+
}
339+
309340
let mut niinii = ui
310341
.window("niinii")
311342
.opened(run)
312343
.menu_bar(true)
313344
.draw_background(!self.settings().transparent);
314345
if no_inputs {
315346
niinii = niinii.no_inputs().draw_background(false);
316-
unsafe { sys::igSetNextFrameWantCaptureMouse(false) }
317347
}
318348
niinii.build(|| {
319349
self.show_menu(ctx, ui);
@@ -323,7 +353,7 @@ impl App {
323353
stroke_text_with_highlight(
324354
ui,
325355
&ui.get_window_draw_list(),
326-
"(interaction disabled, disable ScrLk to re-enable)",
356+
"(interaction disabled)",
327357
1.0,
328358
Some(StyleColor::PlotLinesHovered),
329359
);
@@ -395,6 +425,10 @@ impl App {
395425
if self.show_translator {
396426
self.show_translator(ctx, ui);
397427
}
428+
429+
if no_inputs && !toggle_hovered {
430+
unsafe { sys::igSetNextFrameWantCaptureMouse(false) }
431+
}
398432
}
399433

400434
fn show_settings(&mut self, ctx: &mut Context, ui: &mut Ui) {

niinii/src/renderer/d3d11.rs

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ use winapi::{
1919
d3d11::*,
2020
d3dcommon::*,
2121
winuser::{
22-
self, CallNextHookEx, SetWindowsHookExA, UnhookWindowsHookEx, MSLLHOOKSTRUCT,
23-
WH_MOUSE_LL,
22+
self, CallNextHookEx, SetWindowLongA, SetWindowPos, SetWindowsHookExA,
23+
UnhookWindowsHookEx, HWND_TOPMOST, MSLLHOOKSTRUCT, SWP_FRAMECHANGED, SWP_NOACTIVATE,
24+
SWP_NOMOVE, SWP_NOSIZE, SWP_SHOWWINDOW, WH_MOUSE_LL,
2425
},
2526
},
2627
Interface as _,
2728
};
2829
use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
29-
use winit::window::WindowLevel;
3030
use winit::{
3131
event::{DeviceId, Event, WindowEvent},
3232
event_loop::EventLoop,
@@ -44,6 +44,9 @@ struct Inner {
4444
imgui: imgui::Context,
4545
ctx: Context,
4646

47+
overlay_mode: bool,
48+
last_topmost_refresh: Instant,
49+
4750
renderer: imgui_dx11_renderer::Renderer,
4851
context: ComPtr<ID3D11DeviceContext>,
4952
main_rtv: ComPtr<ID3D11RenderTargetView>,
@@ -78,7 +81,6 @@ impl D3D11Renderer {
7881
pub fn new(settings: &Settings) -> Self {
7982
let event_loop = EventLoop::new().unwrap();
8083

81-
// let on_top = settings.on_top || settings.overlay_mode;
8284
let maximized = settings.overlay_mode;
8385
let decorations = !settings.overlay_mode;
8486
let fullscreen = if settings.overlay_mode {
@@ -92,18 +94,14 @@ impl D3D11Renderer {
9294
.with_transparent(true)
9395
.with_maximized(maximized)
9496
.with_decorations(decorations)
95-
// we set this later, or else we segfault ¯\_(ツ)_/¯
96-
// .with_window_level(if on_top {
97-
// WindowLevel::AlwaysOnTop
98-
// } else {
99-
// WindowLevel::Normal
100-
// })
10197
.with_fullscreen(fullscreen)
10298
.build(&event_loop)
10399
.unwrap();
104100

105101
if settings.overlay_mode {
106102
window.set_cursor_hittest(false).unwrap();
103+
// Don't set this here, or else we segfault
104+
// window.set_window_level(WindowLevel::AlwaysOnTop);
107105
}
108106

109107
let hwnd = match window.raw_window_handle() {
@@ -128,10 +126,10 @@ impl D3D11Renderer {
128126
unsafe { imgui_dx11_renderer::Renderer::new(&mut imgui, device.clone()).unwrap() };
129127

130128
let mut low_level_mouse_proc: Option<HHOOK> = None;
131-
// let mut winit_wnd_proc: winuser::WNDPROC = None;
132129
if settings.overlay_mode {
133-
// this has to be after renderer is created or else we segfault
134-
window.set_window_level(WindowLevel::AlwaysOnTop);
130+
// best-effort: keep window above borderless fullscreen
131+
unsafe { apply_overlay_topmost(hwnd) };
132+
// register low level mouse proc for hittesting abuse
135133
unsafe {
136134
low_level_mouse_proc.replace(SetWindowsHookExA(
137135
WH_MOUSE_LL,
@@ -140,6 +138,7 @@ impl D3D11Renderer {
140138
0,
141139
));
142140

141+
// let mut winit_wnd_proc: winuser::WNDPROC = None;
143142
// let orig_wnd_proc = SetWindowLongPtrA(
144143
// hwnd as HWND,
145144
// winuser::GWL_WNDPROC as i32,
@@ -156,6 +155,8 @@ impl D3D11Renderer {
156155
platform,
157156
imgui,
158157
ctx,
158+
overlay_mode: settings.overlay_mode,
159+
last_topmost_refresh: Instant::now(),
159160
renderer,
160161
context,
161162
main_rtv,
@@ -185,6 +186,8 @@ impl Renderer for D3D11Renderer {
185186
platform,
186187
imgui,
187188
ctx,
189+
overlay_mode,
190+
last_topmost_refresh,
188191
renderer,
189192
context,
190193
main_rtv,
@@ -211,6 +214,18 @@ impl Renderer for D3D11Renderer {
211214
event: winit::event::WindowEvent::RedrawRequested,
212215
..
213216
} => {
217+
// force the window to the top on a best-effort basis, all other approaches have failed
218+
if *overlay_mode
219+
&& last_topmost_refresh.elapsed()
220+
>= std::time::Duration::from_millis(250)
221+
{
222+
let hwnd = match window.raw_window_handle() {
223+
RawWindowHandle::Win32(handle) => handle.hwnd as HWND,
224+
_ => unreachable!(),
225+
};
226+
unsafe { apply_overlay_topmost(hwnd) };
227+
*last_topmost_refresh = Instant::now();
228+
}
214229
unsafe {
215230
context.OMSetRenderTargets(1, &main_rtv.as_raw(), ptr::null_mut());
216231
context.ClearRenderTargetView(main_rtv.as_raw(), &clear_color);
@@ -362,6 +377,22 @@ unsafe fn create_render_target(
362377
ComPtr::from_raw(main_rtv)
363378
}
364379

380+
unsafe fn apply_overlay_topmost(hwnd: HWND) {
381+
let style = winuser::GetWindowLongA(hwnd, winuser::GWL_EXSTYLE);
382+
let mut ex_style = style as u32;
383+
ex_style |= winuser::WS_EX_TOPMOST | winuser::WS_EX_LAYERED | winuser::WS_EX_TOOLWINDOW;
384+
SetWindowLongA(hwnd, winuser::GWL_EXSTYLE, ex_style as i32);
385+
SetWindowPos(
386+
hwnd,
387+
HWND_TOPMOST,
388+
0,
389+
0,
390+
0,
391+
0,
392+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_FRAMECHANGED,
393+
);
394+
}
395+
365396
thread_local! {
366397
static SYSTEM: RefCell<WeakD3D11Renderer> = RefCell::new(WeakD3D11Renderer::new());
367398
}

niinii/src/settings.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub enum TranslatorType {
3232
DeepL,
3333
Chat,
3434
Realtime,
35+
Responses,
3536
}
3637

3738
#[derive(Clone, Deserialize, Serialize)]
@@ -92,6 +93,38 @@ impl Default for RealtimeSettings {
9293
}
9394
}
9495

96+
#[derive(Clone, Deserialize, Serialize)]
97+
#[serde(default)]
98+
pub struct ResponsesSettings {
99+
pub model: openai::ModelId,
100+
pub system_prompt: String,
101+
pub max_output_tokens: Option<u32>,
102+
pub temperature: Option<f32>,
103+
pub top_p: Option<f32>,
104+
pub stream: bool,
105+
pub store: bool,
106+
pub compact_threshold: Option<u32>,
107+
pub reasoning_effort: Option<openai::ReasoningEffort>,
108+
pub verbosity: Option<openai::Verbosity>,
109+
}
110+
impl Default for ResponsesSettings {
111+
fn default() -> Self {
112+
Self {
113+
model: Default::default(),
114+
system_prompt: "You will translate the following visual novel script into English."
115+
.into(),
116+
max_output_tokens: Some(128),
117+
temperature: None,
118+
top_p: None,
119+
stream: true,
120+
store: true,
121+
compact_threshold: None,
122+
reasoning_effort: None,
123+
verbosity: None,
124+
}
125+
}
126+
}
127+
95128
#[derive(Clone, Deserialize, Serialize)]
96129
#[serde(default)]
97130
pub struct Settings {
@@ -116,6 +149,7 @@ pub struct Settings {
116149
pub openai_api_key: String,
117150
pub chat: ChatSettings,
118151
pub realtime: RealtimeSettings,
152+
pub responses: ResponsesSettings,
119153

120154
pub vv_model_path: String,
121155
pub auto_tts_regex: Option<String>,
@@ -153,6 +187,7 @@ impl Default for Settings {
153187
openai_api_key: Default::default(),
154188
chat: Default::default(),
155189
realtime: Default::default(),
190+
responses: Default::default(),
156191

157192
vv_model_path: Default::default(),
158193
auto_tts_regex: None,

niinii/src/translator/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::{settings::Settings, view::View};
66
pub mod chat;
77
pub mod deepl;
88
pub mod realtime;
9+
pub mod responses;
910

1011
#[derive(Error, Debug)]
1112
pub enum Error {

0 commit comments

Comments
 (0)